问题是在编写任何代码之前如何编写测试?我应该创建某种类骨架或接口吗?还是我误会了什么?
扩展他在评论中提出的观点:
然后你编写测试,因为类/任何东西不存在而失败,然后你编写使它们通过的最少量代码
使用 TDD 要记住的一点是,您正在编写的测试是代码的第一个客户端。因此,我不会担心没有定义类或接口——因为正如他所指出的,只需编写引用不存在的类的代码,你就会在循环中得到你的第一个“红色”——即你的代码不会编译!这是一个完全有效的测试。
TDD 也可以表示测试驱动设计
一旦你接受了这个想法,你会发现首先编写测试不再是一个简单的“这段代码是否正确”,而更多的是一个“这段代码是否正确”的指导方针,所以你'你会发现实际上最终生成的产品代码不仅正确,而且结构良好。
现在展示这个过程的视频会很棒,但我没有,但我会举个例子。请注意,这是一个超级简单的示例,忽略了前期的铅笔和纸张规划/业务的实际需求,这通常是您设计过程背后的驱动力。
无论如何,假设我们要创建一个简单的 Person 对象,它可以存储一个人的姓名和年龄。我们想通过 TDD 来做这件事,所以我们知道它是正确的。
所以我们考虑了一分钟,然后编写我们的第一个测试(注意:使用伪 C#/伪测试框架的示例)
public void GivenANewPerson_TheirNameAndAgeShouldBeAsExpected()
{
var sut = new Person();
Assert.Empty(sut.Name);
Assert.Zero(sut.Age);
}
我们马上就有了一个失败的测试,这不会编译,因为 Person 类不存在。因此,您可以使用 IDE 为您自动创建类:
public class Person
{
public int Age {get;set;}
public string Name {get;set;}
}
好的,现在您已经通过了第一次测试。但是现在当您查看该类时,您会意识到没有什么可以确保一个人的年龄始终为正(> 0)。让我们断言是这种情况:
public void GivenANegativeAgeValue_PersonWillRejectIt()
{
var sut = new Person();
Assert.CausesException(sut.Age = -100);
}
好吧,那个测试失败了,所以让我们修复一下这个类:
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;}
}
但是现在你可能会对自己说 - 好吧,既然我知道一个人的年龄永远不可能是 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 void GivenANewPerson_TheirNameAndAgeShouldBeAsExpected()
{
var sut = 新人(42);
Assert.Empty(sut.Name);
断言.42(sut.Age);
}
然后您可能会经历与 Name 类似的过程,依此类推。
现在我知道这似乎是一种非常冗长的创建类的方式,但考虑到您基本上是从头开始设计这个类,并内置了对无效状态的防御 - 例如,您将永远不会必须像这样调试代码:
//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 的主要优点之一,它不仅用作测试工具,而且用作设计工具,让您考虑正在实施的生产代码,并强制执行您需要考虑您创建的类如何最终处于无效、应用程序终止状态,以及如何防范这种情况,并帮助您编写易于使用且不需要其使用者了解它们如何工作的对象,但是而不是他们做什么。
由于任何值得一试的现代 IDE 都可以让您有机会通过几次击键或鼠标点击来创建缺失的类/接口,我相信这种方法非常值得尝试。