在开发可测试软件的过程当中,单元测试已成为确保软件质量的一个不可或缺部分。测试驱动开发(Test-Driven Development,TDD)是编写单元测试的一种方法,采用该方法的开发人员在编写任何产品代码以前都须要编写测试程序。TDD 容许开发人员以系统的方式完善软件设计,从而有效的提升单元测试的质量,增长回归测试(指修改代码后的再次测试)带来的好处。编程
当谈到软件测试时,一般是指进行的一系列不一样种类的测试,包括单元测试、验收测试(acceptance testing)、探索测试(exploratory testing)、性能测试(performance testing)、可扩展性测试(scalability testing)等。并发
大部分的单元测试一般具备如下 4 个特色:app
编写单元测试时,咱们常常查找可以合理测试的最小功能片断。在像 C# 这样的面向对象编程语言中,类一般就意味着是最小的功能片断,但大多数状况下,咱们测试的是类中的一个方法。源代码的阅读次数远超过编写次数,这点在单元测试中特别有用,由于单元测试要测试软件的指望规则和行为。当单元测试失败时,开发人员应该可以快速的阅读测试程序,理解什么出错了,为何会出错,从而可以快速的修正出错的地方。使用小的测试程序来测试小片断代码可以极大的改善测试结果的可理解性。 框架
单元测试另外一个重要方面是它可以在问题出现时精确的指出问题出现的位置。把测试的代码与和它有交互的复杂代码隔离,以确保出现的故障必定是在测试代码中,而不是在与其交互的代码中(检查交互的合做代码是否存在 bug 是合做代码单元测试的任务)。编程语言
在修改类的内部实现时,有时,对代码的一点点修改就可能会致使多个单元测试的失败。所以,在修改产品代码时,维护这些单元测试的人员一般会感到很沮丧。之因此会这样,是由于单元测试对它要测试的类的工做原理了解太多。当编写单元测试时,若是仅局限于产品的公共端(一个组件的集成点),就能够将单元测试与组件的许多内部实现细节相隔离,这样,修改实现细节就不会常常破坏已经编写好的单元测试了。性能
若是对每一小段代码编写测试程序,显而易见,最终咱们会编写不少单元测试。若是测试过程不是自动的,这将会损耗开发人员的大部分精力。为了得到自动过程,开发人员一般使用单元测试框架。框架容许开发人员使用本身擅长的编程语言和开发环境编写测试程序,而后建立 pass / fail 规则集,以后框架会断定测试是否成功。单元测试框架中一般有一个称为运行程序(runner)的小软件,可用来在项目中查找和执行单元测试。有不少这样的软件,一些集成到了 VS 中,一些集成到了 GUI 中,一些须要命令行运行等。单元测试
测试驱动开发指的是利用单元测试来驱动产品代码设计的过程。首先编写单元测试,而后编写产品代码使其经过测试。当把单元测试做为质量保障机制时,主要指的是减小软件中的漏洞。TDD 能够实现这一目标,但这不是它的主要目标;TDD 的主要目标是提升软件设计的质量。经过首先编写单元测试,咱们能够在编写任何产品代码以前描述想要组件执行的操做。因为尚未产品代码的详细实现,所以,咱们不会把精力放到产品代码的任何具体实现上,单元测试变成了产品代码的消费者。测试
仍然遵循前面为单元测试设置的指导原则:编写小段代码、隔离测试和自动执行测试。因为首先编写测试程序,所以当使用 TDD 时,常常会进入一个周期步骤:this
重复以上步骤,直到产品代码编写完毕为止。因为大部分的单元测试框架用红色的 文本 / UI 元素表示失败,用绿色表示经过,所以,这个周期又成为 红 / 绿 周期。spa
“重构”一词具备多种意义,这里是指在不改变产品代码外部可见功能的状况下,修改产品代码实现细节的过程。在重构和更新产品代码的过程当中,单元测试应该可以继续经过。重构时不须要修改任何单元测试程序;若是要求必须修改单元测试,则要按照“红 / 绿 周期”的步骤来添加、删除或改变,切勿同时修改测试程序和产品代码。更确切的说,重构是一种机制,在不破坏单元测试程序的状况下,构建结构化代码的过程。
本文中许多例子都遵守一个成为“Arrange、Act、Assert ”的结构(3A),单元测试的代码以下所示:
[TestMethod]
public void PoppingReturnsLastPushedItemFromStack()
{
// Arrange
Stack<string> stack = new Stack<string>();
string value = "Hello World!";
stack.Push(value);
// Act
string result = stack.Pop();
// Assert
Assert.AreEqual(value, result);
}
arrange 部分建立了一个空栈并推动一个值,这是测试功能的先决条件;act 部分从栈中弹出 arrange 部分添加的值;最后,assert 部分测试一个合乎逻辑的行为:从栈中弹出的值和推动栈中的值是否同样。本例中,assert 部分只有一行代码,难道没有许多其余能够断言(编程术语,表示布尔表达式)的行为吗?例如,一旦从栈中弹出推动的值,栈就变空;难道咱们不该该确保它是空的吗?若是此时再尝试弹出另外一个值,程序就会异常;难道咱们不也应该编写程序测试吗?
在一个测试中,必定不要同时测试多个行为。一个好的单元测试程序一般只测试一个很是小的功能,即一个单一行为。本例测试的是一个非空栈弹出的已知行为,而不是一个空栈的全部属性,不然,应编写更多单元测试。保持测试程序的精简和单一意味着当修改产品代码时只须要修改不多的地方,若是把若干个行为混到一个单元测试(或跨过多个单元测试)中,一个单一行为的破坏可能会致使数十个测试程序的失败,咱们将不得不在每一个测试程序中过滤这几个行为以肯定出现故障的行为。
一些开发人员将这一规则称为 单一断言规则(single assertion rule)。不要误觉得测试程序只能调用一次 Assert,其实,只要记得一次只测试一个行为,而验证一个合乎逻辑的行为调用屡次 Assert 是常常的,有必要的。
尽管能够在 VS 中直接建立单元测试项目,可是开始对 ASP.NET MVC 应用程序进行单元测试须要作大量繁琐的工做。所以,ASP.NET MVC 团队在 New Project 对话框中为 ASP.NET MVC 应用程序包含了单元测试功能:
若是这样,VS 会用一套默认的单元测试来填充新建立的项目,这些默认的单元测试能够帮助新用户理解如何编写 ASP.NET MVC 应用程序的测试程序。
当建立新项目时,系统会自动打开 HomeController.cs 文件,其中包含 3 个操做方法,下面是 Index 操做方法的源代码:
public ActionResult Index()
{
ViewBag.Message = "Modify this template to jump-start your ASP.NET MVC application.";
return View();
}
很是简单,在 ViewBag 中设置欢迎文本并发送给视图,而后返回一个视图结果。在默认的单元测试项目中,Index 操做方法只有一个测试程序:
[TestMethod]
public void Index()
{
// Arrange
HomeController controller = new HomeController();
// Act
ViewResult result = controller.Index() as ViewResult;
// Assert
Assert.AreEqual("Modify this template to jump-start your ASP.NET MVC application.", result.ViewBag.Message);
}
上面是一个很是好的单元测试:按照 3A 形式编写,3 行代码很是容易理解。但尽管这样,该单元测试程序仍然有待完善。虽然 Index 操做方法只有 2 行源代码,却要完成 3 项任务:
但这里的默认单元测试其实是在测试这 3 个问题中的 2个,且存在一个潜在的微妙错误。因为咱们让单元测试尽量精简、单一集中,所以这里至少须要 2 个单元测试,一个用于测试欢迎文本,另外一个用于测试返回的视图结果。固然,若是编写 3 个单元测试,也不为错。
微妙的错误出如今 as 关键字的使用,as 的转换若是与给定的内容不兼容,就会返回 null,而在单元测试程序的 assert 部分,在并无检查返回的视图结果是否为空的状况下,就解引用了 result。这里将该问题标记为待测试的第 4 个问题:操做方法不能返回 null。
as 转换真的是必需的吗?单元测试程序须要 ViewResult 类的一个实例才能访问 ViewBag 属性,这部分没有问题。但咱们对操做方法略做修改:
public ViewResult Index()
{
ViewBag.Message = "Modify this template to jump-start your ASP.NET MVC application.";
return View();
}
修改了返回类型后,也能够清楚的表达代码的功能:Index 老是返回一个视图。这样的一个微小修改,测试的 4 个问题减小为 3 个问题。接下来把前面的测试程序重写成两个:
[TestMethod]
public void IndexShouldAskForDefaultView()
{
HomeController controller = new HomeController();
ViewResult result = controller.Index();
Assert.IsNotNull(result);
Assert.IsNull(result.ViewName);
}
[TestMethod]
public void IndexShouldSetWelcomeMessageInViewBag()
{
// Arrange
HomeController controller = new HomeController();
// Act
ViewResult result = controller.Index();
// Assert
Assert.AreEqual("Modify this template to jump-start your ASP.NET MVC application.", result.ViewBag.Message);
}
这样修改后,看起来测试程序就好多了。as 的转换被省略,更加详细、描述性更强的方法名称使咱们在不查看测试程序的内部代码的状况下,就能够理解测试失败的缘由。咱们可能不知道名为 Index 的测试程序为何会失败,但咱们必定知道名为 IndexShouldSetWelcomeMessageInViewBag 测试失败的缘由。
新的 2 个测试方法有重复的代码,若是进行重构,以后的代码会以下:
private HomeController controller;
private ViewResult result;
[TestInitialize]//标识在测试以前要运行的方法,从而分配并配置测试类中的全部测试所需的资源
public void SetupContext()
{
controller = new HomeController();
result = controller.Index();
}
[TestMethod]
public void IndexShouldAskForDefaultView()
{
Assert.IsNotNull(result);
Assert.IsNull(result.ViewName);
}
[TestMethod]
public void IndexShouldSetWelcomeMessageInViewBag()
{
Assert.AreEqual("Modify this template to jump-start your ASP.NET MVC application.", result.ViewBag.Message);
}
从好的方面来讲,这样减小了代码的重复。但很差的是,移动了测试方法中的 arrange 部分和 act 部分。若是打算以这种方式使用单元测试,那么每一个上下文使用一个测试类最好;另外,当一个单一测试类中添加了数十(或数百)个测试时,支持全部这些测试的必要设置代码就会变得很是多。此时,就不能清楚地知道哪一个单元测试须要哪些设置代码了。