从零开始启动TDD项目

20

我在SO上阅读了很多关于TDD和单元测试的问题和答案,但没有找到回答这个问题的内容:我应该从哪里开始?

我和我的团队已经完成了几个项目,采用了单元测试来测试我们的代码...但是我们的做法是先写代码再写单元测试。在开发过程的某个阶段,很自然地先写测试再写代码,使我们更符合TDD的风格。

现在我们想要迈出下一步,尝试从头开始用TDD开始一个新项目。问题在于...从哪里开始?当我什么代码都没有时,我应该编写哪个测试呢?

假设,只是为了有一个思考的上下文,我必须开发一个互联网应用程序,以文档为中心,有一个小工作流和...其他一些东西。 但是让我们从头开始:首先,我想创建一个简单的页面,列出存储在DB表中的所有文档(元数据)(相当简单,不是吗?)。 我应该编写哪个测试呢?假设我正在使用Hibernate来访问数据库...我应该测试假设的方法getAllDocuments()吗?但是我应该使用模拟对象来替换Hibernate吗?那我在测试什么呢?

我有点困惑...而且getAlDocuments()可能永远不会成为一个生产方法...所有文件的集合都将按某种方式进行排序和过滤...这有意义吗? 任何建议都将不胜感激

编辑:

在阅读了您的答案(以及http://programmers.stackexchange.com中类似的线程)之后,我对TDD有了更好的理解,但我仍然有疑问。

我一直认为TDD是关于先编写单元测试...从来没有想到过端到端测试。 但是让我问一下:TDD说您必须先编写测试并看到编译错误;然后创建类和方法,然后获得测试失败;然后实现该方法并通过测试。在有测试失败之前,您不能编写代码;在所有测试通过之前,您不能编写另一个测试。我在这里是正确的吗?

我应如何将端到端测试作为我的第一个测试?我应编写所有层的代码,以让该测试通过。但这样一来,我的端到端测试(虽然应该称为集成测试)会测试一堆类和方法。这意味着我不再需要单元测试,因为我已经有了一个覆盖我的代码的测试。但是,我不能编写已经通过的测试,这违反了TDD原则。
请帮助我更深入地理解这一步骤。

我通常从编写自动加载器组件的测试开始。然后为每个类添加测试。 - hakre
昨天刚问了同样的问题。 - Carl Manaster
4个回答

15

TDD不是关于单元测试,而是使用任何类型的自动化测试来推动您的开发和架构 - 您需要进行测试。你明白这个意思吗?

当您开始一个新项目时,您可能有一些功能要实现。您应该为您要实现的功能制定一些验收标准。这些标准可以定义您的顶级测试。让我们从端到端测试(这有时可能很困难,因为它涉及尚不存在的UI)或针对这些验收标准的集成测试开始。一旦您有了失败的测试,您将继续实现与大型测试相关的功能,但每个功能都将再次使用集成或单元测试进行驱动。如果所有顶级测试都通过,则完成该功能。

如果您跳过大型测试(端到端,集成),则会开发一组经过良好测试的单位,这些单位在集成在一起时可能无法正常工作,或者由于单元测试定义的局部范围而导致架构不太好。集成和端到端测试可以给您提供全局范围。

这在书籍《Growing Object-Oriented Software Guided by Tests》中用例子(Java)进行了详细介绍。


请查看我的更新问题,并帮助我更深入地理解这个概念。 - themarcuz
1
我已经查看了您的更新。我确认我所描述的版本不同,也没有遵循这种方法(但肯特·贝克描述的早期TDD也没有包括这些规则)。我的描述基于我参考的书籍,我发现它更有用。无论如何,您仍然可以调整您的流程,并在单个不完整的单元测试中遵循规则,但在进行单元测试时允许不完整的集成测试。 - Ladislav Mrnka

2

既然你想根据测试驱动开发,那么开始的方式就是从第一个功能开始。例如,假设你有一个上传文档的功能。你的第一个测试可能是:

public class DocumentManagementTest {
  @Test public void allowsDocumentUploads() {
    DocumentManagement dm = new DocumentManagement();
    Reader mockReader = new MockDocumentReader();

    Document result = dm.createDocument("Document name", mockReader);

    assertEquals("Document name", result.getName());
    assertEquals(0, result.getTags().size());
    assertTrue(mockReader.fileWasRead);
  }
}

我建议一开始使用模拟数据库,因为设置和撤销数据库是很费力且容易出错的。但要记住采取非常小的步骤,上面所示的测试可能会在几次迭代中逐渐演变。后续的测试可以推动更多的设计工作:

@Test public void allowsDocumentRenames() { ... }
@Test public void allowsAddingTagsToExistingDocuments() { ... }
@Test public void showsErrorWhenAddingDocumentThatAlreadyExists() { ... }

一旦你构建了一个功能,比如createDocument,你可以创建一个控制器来管理它。

public void doPost(HttpServletRequest req, HttpServletResponse resp) {
  String name = req.getParameter("doc_name");
  Document d = docMgmt.createDocument(name, req.getInputStream());
  // Hand the newly created document to the view engine.
}

我认为不必过于担心为控制器编写测试,因为从复杂度的角度来看,它是相当低风险的(如果控���器代码量太多,则可能是一种让控制器代码属于另一个类的信号,可能是您的DocumentManagement类)。

通过逐步构建功能并遵循SOLID原则,您将慢慢构建出一个具有良好测试覆盖率和相当不错的OO属性的系统。

干杯!

Brandon


2

从简单的开始,稍后再逐步添加功能。从快速设计开始:哪些类、哪些职责、哪些关系。您可以使用CRC卡片。不要花太多时间在这个设计上,因为您可以通过重构来改进它。选择最简单的类来开始实现系统的简单功能。例如,您可以首先创建一个空页面。

从一个类开始——它的对象应该做什么?如何验证这样做是正确的?这是第一个测试。

您也可以开始时不使用数据库,并将文档存储在平面文件中。稍后再重构到数据库。然后您可以从getAllDocuments()函数开始。


2

我通常从上到下开始。在您的情况下,我会从编写新页面的控制器逻辑开始。通过控制器,我指的是UI下面的代码层,模拟下面的所有内容。然后编写服务层(如果您有一个),模拟数据层。最后使用底层类的模拟(在您的情况下可能是ISession)测试数据层。最后,我会编写每个数据层方法的单个集成测试,并构建页面(HTML)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接