如何在现实世界的项目中选择TDD起点?

时间:2011-11-02 21:11:26

标签: unit-testing tdd

我阅读了大量的文章,看过大量有关TDD的截屏视频,但我仍在努力在现实世界的项目中使用它。我的主要问题是我不知道从哪里开始,哪个测试应该是第一个。 假设我必须编写客户端库来调用外部系统的方法(例如通知)。 我希望这个客户端如下工作

NotificationClient client = new NotificationClient("abcd1234"); // client ID
Response code = client.notifyOnEvent(Event.LIMIT_REACHED, 100); // some params of call

幕后有一些翻译和消息格式准备,因此我想将其隐藏在我的客户端应用程序中。

我不知道在哪里以及如何开始。 我应该为这个库设置一些粗略的课程吗? 我应该开始测试NotificationClient,如下所示

public void testClientSendInvalidEventCommand() {
    NotificationClient client = new NotificationClient(...);
    Response code = client.notifyOnEvent(Event.WRONG_EVENT);
    assertEquals(1223, code.codeValue());
}

如果是这样的话,通过这样的测试,我不得不立即编写完整的工作实现,没有像TDD那样的婴儿步骤。我可以在客户端嘲笑sosmething,但后来我必须知道这个事情要提前嘲笑,所以我需要一些前期设计。

也许我应该从底部开始,首先测试此消息格式化组件,然后在正确的客户端测试中使用它?

正确的方法是什么? 我们应该始终从最高层开始(如何处理这个需要的巨大步骤)? 我们可以从实现所需功能的微小部分的任何类开始(在本例中为Formatter)吗?

如果我知道在哪里接受我的测试,我会更容易继续。

4 个答案:

答案 0 :(得分:2)

我从这一行开始:

NotificationClient client = new NotificationClient("abcd1234"); // client ID

听起来我们需要一个NotificationClient,它需要一个客户端ID。这是一个容易测试的东西。我的第一个测试可能看起来像:

public void testNewClientAbcd1234HasClientId() {
    NotificationClient client = new NotificationClient("abcd1234");
    assertEquals("abcd1234", client.clientId());
}

当然,它最初不会编译 - 直到我编写一个NotificationClient类,其构造函数接受一个字符串参数和一个返回字符串的clientId()方法 - 但这是TDD循环的一部分。

public class NotificationClient {
    public NotificationClient(string clientId) {
    }
    public string clientId() {
        return "";
    }
}

此时,我可以运行我的测试并观察它失败(因为我已将硬编码的clientId()返回为空字符串)。一旦我完成了失败的单元测试,我会编写足够的生产代码(在NotificationClient中)以使测试通过:

    public string clientId() {
        return "abcd1234";
    }

现在我的所有测试都通过了,所以我可以考虑接下来要做什么。显而易见的(很明显, me )下一步是确保我可以创建ID不是“abcd1234”的客户端:

public void testNewClientBcde2345HasClientId() {
    NotificationClient client = new NotificationClient("bcde2345");
    assertEquals("bcde2345", client.clientId());
}

我运行我的测试套件并观察testNewClientBcde2345HasClientId()在testNewClientAbcd1234HasClientId()通过时失败,现在我有充分的理由将成员变量添加到NotificationClient:

public class NotificationClient {
    private string _clientId;
    public NotificationClient(string clientId) {
        _clientId = clientId;
    }
    public string clientId() {
        return _clientId;
    }
}

假设没有打字错误,那将让我的所有测试都通过,我可以继续下一步。 (在您的示例中,可能会测试notifyOnEvent(Event.WRONG_EVENT)返回Response等于{12}的codeValue()

这对你有帮助吗?

答案 1 :(得分:1)

请勿将acceptance tests与您的应用程序的每一端混淆,并与executable specifications形成unit tests

如果您正在进行'纯'TDD,那么您可以编写一个验收测试来驱动实施的单元测试。 testClientSendInvalidEventCommand是您的验收测试,但根据事情的复杂程度,您可以将实现委托给多个类,您可以单独进行单元测试。

在必须将它们拆分以进行测试并正确理解之前,事情变得多么复杂,这就是为什么它被称为测试驱动设计

答案 2 :(得分:1)

您可以选择让测试从下到上或从上到下驱动您的设计。两者都适用于不同情况下的不同开发人员。任何一种方法都会强制做出一些“前期”设计决定,但那是件好事。为了编写测试而做出这些决定是测试驱动的设计!

在你的情况下,你知道你正在开发的系统的高级外部接口应该是什么,让我们从那里开始。编写一个测试,看看您认为通知客户端的用户应该如何与之交互并让它失败。此测试是您的验收或集成测试的基础,它们将继续失败,直到它们描述的功能完成。没关系。 现在下一级。提供高级接口需要采取哪些步骤?我们可以为这些步骤编写集成或单元测试吗?他们是否有您未考虑的依赖关系,这可能会导致您更改已开始定义的通知中心界面?继续钻取深度优先定义行为并进行失败的测试,直到您发现实际已达到单元测试。现在实现足以通过该单元测试并继续。获取单元测试,直到您构建足够的内容以通过集成测试等。您最终将完成测试树的深度优先构建,并且应该具有经过良好测试的功能,其设计由您的测试驱动。

答案 3 :(得分:0)

TDD的一个目标是测试通知设计。因此,您需要考虑如何实施NotificationClient是一件好事;它迫使你在前面想到(希望)简单的抽象。

此外,TDD类型假设不断重构。你的第一个解决方案可能不会是最后一个;因此,当您优化代码时,测试就会告诉您从编译错误到实际运行时问题的中断。

所以我会直接跳到你建议的测试开始。在创建模拟时,您需要为您正在模拟的实际实现创建测试。你会发现事情很有意义,需要重构,所以你需要随时修改你的测试。这就是它的工作方式......