TDD和单元测试

这篇博客的动机是解释测试驱动开发中使用的测试的性质和目的。为了避免混淆,我将使用表达式TDD测试来引用测试驱动开发环境中使用的测试类型。这篇博客文章的目的是澄清TDD测试、单元测试和验收测试之间的关系。

TDD Tests are not Unit Tests

让我们从TDD测试和单元测试之间的区别开始。表面上,TDD测试与单元测试非常相似。这并不奇怪,因为您使用单元测试框架,如Visual Studio Tests或NUnit来创建这两种类型的测试。

单元测试的目的是独立测试代码单元。例如,您可以创建一个单元测试来验证特定的类方法是否返回您期望的值。单元测试的标准例子是验证数学类的Add()方法的测试(参见Listing 1)。

Listing 1 – Math.cs

1
2
3
4
5
6
7
public class Math
{
public int Add(int val1, int val2)
{
return val1 * val2;
}
}

Listing 2 – MathTests.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[TestClass]
public class MathTests
{

[TestMethod]
public void TestAdd()
{
// Arrange
var math = new Math();

// Act
var result = math.Add(2, 3);

// Assert
Assert.AreEqual(5, result);
}
}

Listing 1中的Add()方法旨在将两个数字相加(但是写得不好)。

Listing 2中的单元测试验证了2 + 3实际上等于5。如果Add()方法没有正确实现,那么单元测试就会失败(参见图1)。

图1–单元测试失败

在现实生活中,情况很少像这个数学例子那样简单。通常,一个类对其他类有许多依赖性。例如,清单3中的类包含验证逻辑。但是,它也调用数据访问类和日志记录类。在这些情况下,您必须求助于伪造或存根依赖类。

Listing 3 – Validation.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Validation
{
private ForumsRepository _repository = new ForumsRepository();
private Logger _logger = new Logger();


public void CreateForumPost(ForumPost post)
{
// Validate forum post
if (String.IsNullOrEmpty(post.Subject))
throw new Exception("Subject is required!");

// Store forum post in database
_repository.CreateForumPost(post);

// Log it
_logger.Log("Add new forum message");
}

}

在设计良好的应用程序中,每个类都有一个单独的职责(SRP原则)。验证类只包含验证逻辑,数据访问类只包含数据访问逻辑,依此类推。创建单元测试时,测试一个类是否满足其职责。例如,当单元测试验证类时,不测试数据访问逻辑。验证类的单元测试应该验证验证逻辑的行为是否符合您的预期。

单元测试的目的是让开发人员相信他们的应用程序的特定部分以开发人员期望的方式运行。这对回归测试非常有价值。如果您修改了单元测试所涵盖的代码,那么您可以使用这些测试来立即确定您是否已经破坏了现有的功能。

那么TDD测试和单元测试有什么不同呢?与单元测试不同,TDD测试用于驱动应用程序的设计。TDD测试用于表示在实际编写应用程序代码之前,应用程序代码应该做什么。

测试驱动开发源于对瀑布开发的反应。测试驱动开发的一个重要目标是肯特·贝克所说的增量设计和马丁·福勒所说的进化设计。应用程序不是一次设计一个应用程序,而是一个一个测试地递增设计。

在实践测试驱动开发时,您可以通过反复执行以下步骤来开发应用程序:

  1. 创建失败的测试
  2. 编写足够的代码通过测试
  3. 重构代码以改进其设计

这个过程被称为红色/绿色/重构,因为单元测试框架显示失败测试的红色条和通过测试的绿色条。

当实践测试驱动开发时,第一步总是创建一个失败的测试。您使用测试来表达您希望代码如何表现。例如,在构建论坛应用程序时,您可以从编写一个测试开始,验证您是否可以向论坛发布新消息。

当您编写TDD测试时,您编写测试时没有预先做出任何设计决策。福勒写道:

然而,古典主义者认为,重要的是只考虑外部接口发生了什么,并且在完成测试之前不考虑实现

http://martinfowler.com/articles/mocksArentStubs.html#DrivingTdd

当编写单元测试时,你应该只考虑你想要你的应用程序做什么。写完测试后,就可以做出实现决定了。最后,在实现了足够多的代码之后,您需要考虑如何重构应用程序以获得更好的设计。

与单元测试不同,TDD测试可能一次测试多个代码单元。福勒写道:

在极限编程中,单元测试通常不同于经典的单元测试,因为在极限编程中,您通常不会单独测试每个单元。

Fowler http://www.artima.com/intv/testdriven4.html

随着应用程序设计的发展,最初包含在一个类中的代码可能最终会出现在多个类中。与单元测试不同,TDD测试可以测试跨多个类的代码。

TDD Tests are not Acceptance Tests

客户验收测试(也称为集成测试或功能测试)用于验证应用程序是否按照客户期望的方式运行。验收测试通常是与客户合作创建的,作为确定客户要求是否得到满足的一种方式。

接受测试的观众不同于TDD测试的观众。验收测试是为了客户的利益而创建的。另一方面,TDD测试是为开发人员的利益而创建的。

与TDD测试不同,验收测试不是用单元测试框架创建的。相反,客户验收测试是用验收测试框架创建的,如硒、适合、适合或瓦蒂尔/瓦丁。

这些验收测试框架使您能够端到端地测试应用程序。例如,使用硒,您可以模拟向网络服务器发布一个HTML表单。然后,您可以验证网络服务器是否返回了特定的响应(您可以对响应中返回的文本进行模式匹配)。

TDD测试和验收测试之间的一个重要区别是,当您执行验收测试时,一切都是连接在一起的。您对实际的网络服务器和数据库服务器执行验收测试。

另一方面,TDD测试需要执行得非常快。每次对代码进行更改时,都应该执行TDD测试。因为一个应用程序可能包含数百(甚至数千)个TDD测试,所以单个TDD测试必须很快。

肯特·贝克称之为10分钟规则。你应该能够在不到10分钟的时间内完成所有的TDD测试(并且还有时间喝一杯咖啡)。如果您正在对实际的网络服务器或数据库运行TDD测试,那么您将无法满足10分钟规则。

What is a TDD Test?

理解TDD测试目的的最好方法是理解测试驱动开发的目的。福勒说:

TDD的起源是希望获得强大的支持进化设计的自动回归测试。

http://martinfowler.com/articles/mocksArentStubs.html#DrivingTdd

首先,像单元测试一样,TDD测试支持回归测试。每当您对应用程序代码进行更改时,您都可以使用TDD测试来快速验证您是否已经破坏了现有的应用程序功能。这种对回归测试的支持使增量设计成为可能,因为它使您能够不断重构您的应用程序。你可以无情地重构你的应用程序以获得更好的设计,因为你有单元测试的安全网。

第二,TDD测试——像验收测试——用来驱动应用程序的设计。TDD测试充当迷你验收测试。TDD测试表达了你下一步需要做的任务和成功的标准。

测试驱动开发有几个很好的演练,包括:

在实践测试驱动开发时,您从一系列用户故事开始,这些故事以非技术的方式描述了应用程序应该如何运行。这些用户故事是与应用程序的客户合作开发的。每个用户故事最多应该有几个句子长。

在肯特·贝克的演练中,他保留了一个看起来非常像任务列表的任务列表。执行一项任务会激励他向列表中添加额外的任务。当他创建了一个与任务相对应的测试,并实现了通过测试所必需的代码时,他将任务从列表中划掉。

例如,在构建论坛网络应用程序时,您可以从以下用户故事列表开始:

  • 用户应该能够创建新的论坛帖子。论坛帖子包括作者、主题和正文。
  • 用户应该能够在主页上查看所有论坛帖子。最新的帖子应该比以前的帖子高。
  • 用户应该能够回复现有帖子。
  • 用户应该能够选择一个特定的帖子并看到该帖子和所有的回复。

用户故事列表随着您编写应用程序和从客户那里获得额外反馈而发展。您可以使用用户故事的初始列表作为起点。

接下来,您选择一个要实现的用户故事。您的TDD测试应该从用户故事中流出。例如,假设您选择这个故事来实现:

用户应该能够创建新的论坛帖子。论坛帖子包括作者、主题和正文。

在这种情况下,您可以从清单4所示的TDD测试开始。清单4中的测试验证了您可以向论坛应用程序添加新帖子。

Listing 4 – ForumControllerTests.cs

1
2
3
4
5
6
7
8
9
10
[TestMethod]
public void ForumPostValid()
{
// Arrange
var controller = new ForumController();
var postToCreate = new ForumPost();

// Act
controller.Create(postToCreate);
}

该测试验证您可以用新的论坛帖子在论坛控制器上调用Create()操作。编写测试后,您可以实现满足测试所需的应用程序代码。在这种情况下,您需要创建一个论坛控制器和论坛类。

在您创建第一个测试并编写代码来满足测试之后,您将会想到其他相关的测试。例如,您可能希望验证发布没有主题的论坛消息会生成验证错误消息。清单5中的测试验证了当您试图发布没有主题的消息时,验证错误消息被添加到模型状态。

Listing 5 – ForumControllerTests.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[TestMethod]
public void ForumPostSubjectIsRequired()
{
// Arrange
var controller = new ForumController();
var postToCreate = new ForumPost { Subject = string.Empty };

// Act
var result = (ViewResult)controller.Create(postToCreate);

// Assert
var subjectError = result.ViewData.ModelState["Subject"].Errors[0];
Assert.AreEqual("Subject is required!", subjectError.ErrorMessage);
}

这里的要点是TDD测试直接来自用户故事。TDD测试就像迷你验收测试一样。您正在使用TDD测试来驱动应用程序的构建。

Conclusion

这篇博客文章的目的是澄清TDD测试、单元测试和验收测试之间的区别。TDD测试类似于单元测试和验收测试,但并不相同。

像单元测试一样,TDD测试可以用于回归测试。您可以使用TDD测试来立即确定代码的更改是否破坏了现有的应用程序功能。然而,与单元测试不同,TDD测试不一定要单独测试一个代码单元。

像验收测试一样,TDD测试用于驱动应用程序的创建。TDD测试像迷你验收测试一样工作。您创建一个TDD测试来表达下一步需要实现的应用程序功能。然而,与验收测试不同,TDD测试不是端到端测试。TDD测试不与实时数据库或网络服务器交互。

原文链接:TDD Tests are not Unit Tests

推荐文章