写代码前先写测试

Writing tests before writing code

据我了解,TDD 和 BDD 周期是这样的:

  1. 从编写测试开始
  2. 看到他们失败了
  3. 编写代码
  4. 通过测试
  5. 重复

问题是在没有任何代码之前如何编写测试?我应该创建某种 class 骨架或界面吗?还是我误解了什么?

是的,没错。如果您在 Rails 上查看 Michael Hartl 关于 Ruby 的书(HTML 免费观看),您将看到他具体是如何做到这一点的。因此,为了补充 lared 所说的内容,假设您的第一份工作是向网页添加一个新按钮。您的流程将如下所示:

  1. 编写一个测试以直观地查找页面上的按钮。
  2. 验证测试是否失败(不应存在按钮,因此它应该会失败)。
  3. 编写代码以在页面上放置按钮。
  4. 验证测试通过。

当你不小心对你的代码做了一些破坏旧测试的事情时,TDD 将拯救你的培根。例如,您不小心将按钮更改为 link。测试将失败并提醒您注意问题。

你有它的本质,但我会改变你的描述的一部分。您不会在编写代码之前编写 tests - 在编写代码之前编写 a test。然后——在编写更多测试之前——你只需要编写足够的代码来让你的测试通过。当它通过时,您会寻找机会改进代码,并在保持测试通过的同时进行改进——然后编写第二个测试。关键是,您在任何给定时间都专注于一小部分功能。您希望您的程序接下来做什么?为此编写一个测试,仅此而已。让测试通过。清理代码。你想让它做的下一件事是什么?迭代直到你满意。

问题是,如果你在写代码之前写 tests,你就没有那个重点。一次一个测试。

如果你使用的是真正的编程语言,(你知道,有编译器等等)那么是的,你当然必须编写 class 骨架或接口,否则你的测试甚至无法编译.

如果您使用的是脚本语言,那么您甚至不必编写框架或接口,因为您的测试脚本会愉快地开始 运行 并且会在第一个不存在的 class 或遇到的方法。

TDD 和 BDD 是不同的事物,但它们共享一个共同的机制。这种共享机制是先写一些 'tests' 的东西,然后再写做某事的东西。然后,您使用失败来 guide/drive 发展。

(

您通过思考要解决的问题来编写测试,并通过假装您拥有可以测试的理想解决方案来充实细节。您编写测试以使用理想的解决方案。这样做会做各种各样的事情,比如:

  1. 发现解决方案所需的事物的名称
  2. 发现你的东西的接口,使它们易于使用
  3. 经历过失败的事情 ...

BDD 和 TDD 之间的区别在于 BDD 更侧重于 'what' 和 'why',而不是 'how'。 BDD 非常关注使用适当的语言来描述事物。 BDD 从更高的抽象级别开始。当你到达细节压倒语言的领域时,TDD 被用作实现细节的工具。

你可以选择思考事物并在不同的抽象层次上写下它们的想法是关键。

你写'tests'你需要的选择:

  1. 适合您问题的语言
  2. 适当的抽象级别来简单明了地解释您的问题
  3. 调用您的功能的适当机制。

The question is how do you write tests before you have any code? Should I create some kind of class skeletons or interfaces? Or have I misunderstood something?

进一步阐述他在评论中提出的观点:

Then you write tests, which fail because the classes/whatever doesn't exist, and then you write the minimal amount of code which makes them pass

TDD 要记住的一件事是,您正在编写的测试是您代码的第一个客户端。因此,我不会担心没有定义 classes 或接口 - 因为正如他指出的那样,只需编写引用不存在的 classes 的代码,你就会得到你的第一个 "Red" 在循环中 - 也就是说,您的代码将无法编译!这是一个完全有效的测试。

TDD 也可以表示测试驱动设计

一旦你接受了这个想法,你会发现首先编写测试并不是一个简单的 "is this code correct" 而更多的是一个 "is this code right" 指南,所以你会发现你实际上最终生成不仅正确而且结构良好的生产代码。

现在如果有一个视频展示这个过程就太棒了,但我没有,但我会尝试举一个例子。请注意,这是一个 超级简单 示例,它忽略了预先的纸笔计划/来自业务的现实世界要求,这通常是您设计过程背后的驱动力。

无论如何假设我们想要创建一个简单的 Person 对象来存储一个人的姓名和年龄。我们想通过 TDD 来做到这一点,所以我们知道它是正确的。

所以我们考虑一下,然后编写我们的第一个测试(注意:示例使用伪 C#/伪测试框架)

public void GivenANewPerson_TheirNameAndAgeShouldBeAsExpected()
{
        var sut = new Person();
        Assert.Empty(sut.Name);
        Assert.Zero(sut.Age);
}

立即我们有一个失败的测试,这不会编译,因为 Person class 不存在。因此,您使用 IDE 为您自动创建 class:

public class Person
{
    public int Age {get;set;}
    public string Name {get;set;}
}

好的,现在你已经通过了第一次测试。但是现在当您查看 class 时,您会意识到没有什么可以确保一个人的年龄始终为正数 (>0)。让我们断言是这样的:

public void GivenANegativeAgeValue_PersonWillRejectIt()
{
    var sut = new Person();
    Assert.CausesException(sut.Age = -100);
}

好吧,那个测试失败了,所以让我们修复 class:

public class Person
{
    protected int age;
    public int Age 
    {
        get{return age;}
        set{
                if(value<=0) 
                {
                    throw new InvalidOperationException("Age must be a positive number");
                }
                age=value;
            }
        }
        public string Name {get;set;}
}

但现在你可能会对自己说 - 好吧,既然我 知道 一个人的年龄永远不会 <=0,我为什么还要费心创建一个可写的 属性 - 我是否总是想写两条语句,一条创建 Person,另一条设置它们的 Age?如果我忘记在我的代码的一部分中这样做怎么办?如果我在我的代码的一部分中创建了一个 Person,然后稍后我试图在另一个模块中将一个负变量分配给 Age 会怎样?当然,Age 必须是 Person 的不变量,所以让我们解决这个问题:

public class Person
{
    public Person(int age){
        if (age<=0){
            throw new InvalidOperationException("Age must be a positive number");
        }
        this.Age = age;
    }   
    public int Age {get;protected set;}
    public string Name {get;set;}
}

当然你必须修复你的测试,因为它们将不再编译 - 如果事实上现在你意识到第二个测试是多余的并且可以删除!

public无效GivenANewPerson_TheirNameAndAgeShouldBeAsExpected() { var sut = new Person(42); Assert.Empty(sut.Name); 断言.42(sut.Age); }

然后您可能会对 Name 进行类似的过程,依此类推。 现在我知道这似乎是一种非常冗长的创建 class 的方法,但考虑到您基本上是从头开始设计这个 class 的,并且内置了针对无效状态的防御措施 - 例如,您将 永远不会必须像这样调试代码:

//A Person instance, 6,000 lines and 3 modules away from where it was instantiated
john.Age = x; //Crash because x is -42

 //A Person instance, reserialised from a message queue in another process
    var someValue = 2015/john.Age; //DivideByZeroException because we forgot to assign john's age 

对我来说,这是 TDD 的主要优势之一,它不仅可以用作测试工具,还可以用作设计工具,让您思考生产代码正在实施,并迫使您考虑您创建的 classes 如何最终进入无效的应用程序终止状态,以及如何防范这种情况,并帮助您编写易于使用且不易使用的对象要求他们的消费者了解他们的工作方式,而不是他们的工作。

由于任何现代的 IDE 值得一试,您都可以通过几次击键或鼠标点击来创建缺失的 classes / 界面,我相信这种方法值得一试.