我阅读了大量的文章,看过大量有关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)吗?
如果我知道在哪里接受我的测试,我会更容易继续。
答案 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类型假设不断重构。你的第一个解决方案可能不会是最后一个;因此,当您优化代码时,测试就会告诉您从编译错误到实际运行时问题的中断。
所以我会直接跳到你建议的测试开始。在创建模拟时,您需要为您正在模拟的实际实现创建测试。你会发现事情很有意义,需要重构,所以你需要随时修改你的测试。这就是它的工作方式......