在一个复杂的应用程序中如何实施测试驱动开发(TDD)?

15

我已经阅读了许多有关TDD的书籍和网站,它们都很有道理,特别是Kent Beck的书。然而,当我尝试自己进行TDD时,我发现自己盯着键盘想开始从哪里下手。您是否有使用的过程?您的思维过程是什么?您如何确定您的第一个测试?

大多数关于此主题的书籍都很好地描述了TDD是什么,但并未说明如何在真实的非平凡应用中实践TDD。您如何进行TDD?

8个回答

5
其实,这比你想象的要容易。您只需在每个单独的类上使用TDD。类中的每个公共方法都应该针对所有可能的结果进行测试。因此,您所看到的“概念验证”TDD示例也可以用于具有许多类的相对较大的应用程序。
另一种可以使用的TDD策略是通过封装主应用程序行为来模拟应用程序测试运行本身。例如,我编写了一个框架(在C ++中,但这适用于任何OO语言),它表示一个应用程序。有用于初始化、主运行循环和关闭的抽象类。因此,我的main()方法看起来像这样:
int main(int argc, char *argv[]) {
  int result = 0;

  myApp &mw = getApp(); // Singleton method to return main app instance
  if(mw.initialize(argc, argv) == kErrorNone) {
    result = mw.run();
  }

  mw.shutdown();
  return(result);
}

这样做的好处有两个。首先,所有主要应用程序功能都可以编译成静态库,然后链接到测试套件和main.cpp存根文件中。其次,这意味着我可以通过为argc和argv[]创建数组,然后模拟main()中会发生的事情来模拟整个主应用程序的“运行”。我们使用此过程来测试许多真实世界的功能,以确保应用程序在给定某个真实世界输入数据和命令行参数的情况下生成完全符合预期的结果。
现在,您可能想知道对于具有真正GUI、基于Web的接口或其他方面的应用程序,这将如何改变。对此,我只会建议使用模型来测试程序的这些方面。
简而言之,我的建议归结为:将测试用例分解为最小级别,然后开始向上查看。最终,测试套件将将它们全部组合在一起,您将获得合理水平的自动化测试覆盖率。

4
我曾经有同样的问题。我过去常常通过启动一个窗口设计器来创建我想实现的第一个功能的用户界面。由于UI是最难测试的东西之一,这种工作方式并不非常适合TDD。
我发现Atomic Object关于Presenter First的论文非常有帮助。我仍然从构思我想要实现的用户操作开始(如果你有用例,那是一个很好的开始),使用MVP或MVC-ish模型,我从编写第一个屏幕的Presenter测试开始。通过模拟视图直到Presenter可行,我可以以这种方式快速入门。http://www.atomicobject.com/pages/Presenter+First 这里提供了更多关于这种工作方式的信息。
如果你正在使用你不熟悉或有许多未知内容的语言或框架开始一个项目,你可以先进行一个Spike。我经常为我的Spike编写单元测试,但只运行我正在进行Spike的代码。进行Spike可以为你提供一些关于如何开始你的真正项目的输入。不要忘记在开始真正的项目时放弃你的Spike。

1
请问"doing a spike"的意思是什么?我之前从没听说过这个术语。你是不是指原型(prototype)? - Ben Aston
1
Spike源自XP(极限编程),它比原型更小(我的Spike通常需要半小时到一小时)。这意味着编写一次性代码来尝试一小部分技术。因为你不是在编写生产代码,所以可以进行Spike而无需测试和重构。 - Mendelt

3
我从需求开始思考。
对于每个用例,
  1. 分析用例
  2. 考虑未来的类
  3. 写下测试用例
  4. 编写测试
  5. 测试和实现类(有时在第4点遗漏内容时添加新的测试)。
就这样。这很简单,但我认为它需要花费大量时间。尽管如此,我喜欢它并坚持使用它。 :)
如果我有更多时间,我会尝试在Enterprise Architect中建模一些顺序图。

1

问题在于你正在盯着键盘想要写哪些测试。

相反,先考虑你想要编写的代码,然后找到其中的第一个小部分,再尝试思考可以迫使你编写该小部分代码的测试。

一开始最好是以非常小的片段来工作。即使在一天内,你也会逐渐地处理更大的代码块。但任何时候,当你卡住了,只需考虑下一个你想要编写的最小代码片段,然后为其编写测试。


1

我同意,启动这个过程确实很难。

我通常尝试将第一组测试想象成电影剧本,也许只是电影的第一场景。

演员1告诉演员2世界有麻烦了,演员2递回一个包裹,演员1打开包裹等等。

显然这是一个奇怪的例子,但我经常发现通过可视化交互是克服最初障碍的好方法。还有其他类似的技术(用户故事、RRC卡等),对于更大的团队也很有效,但听起来你是独自一人,可能不需要额外的负担。

此外,我相信你最不想做的事情就是再读一本书,但MockObjects.com的人们正在撰写一本早期草稿阶段的书,目前标题为Growing Object-Oriented Software, Guided by Tests。目前供审查的章节可能会给你一些关于如何开始TDD并持续进行下去的进一步见解。


感谢您的推荐。Bootstrapping是一个很好的术语。 - Its me

0

有时候你不知道如何进行TDD,因为你的代码不是“测试友好”的(易于测试)。

通过一些良好的实践,你的类可以变得更容易被隔离地测试,从而实现真正的单元测试。

我最近发现了一篇谷歌员工的博客,其中描述了如何设计你的类和方法,使它们更容易测试。

这是他最近的一个演讲,我推荐观看。

他强调了将业务逻辑与对象创建代码分离(即避免将逻辑与“new”运算符混合在一起),使用依赖注入模式。他还解释了德米特法则对可测试代码的重要性。他主要关注Java代码(和Guice),但他的原则应该适用于任何语言。


Miško Hevery的演讲非常棒,强烈推荐。 - Nazgob

0
最简单的方法是从没有依赖关系的类开始,这个类被其他类使用,但不使用另一个类。然后你应该选择一个测试,问自己“如果这个类(这个方法)实现正确,我怎么知道?”。
然后你可以编写第一个测试来查询未初始化的对象,它可能返回NULL或抛出异常。然后你可以初始化(可能只是部分地)你的对象,并测试它是否返回有价值的东西。然后你可以添加另一个初始化值的测试 - 应该表现出相同的行为。此时,我通常会测试一个错误条件 - 比如尝试用无效值初始化对象。
当你完成了这个方法,就去处理同一个类的另一个方法,直到整个类都完成为止。
然后你可以选择另一个类 - 要么是另一个独立的类,要么是使用你已经实现的第一个类的类。
如果你选择一个依赖于你的第一个类的类,我认为在测试环境或第二个类实例化第一个类时是可以接受的,因为它已经经过充分测试。当一个关于类的测试失败时,你应该能够确定问题出在哪个类中。
如果您在第一类中发现问题,或者想知道它在某些特定条件下是否能正确运行,则应编写新的测试。
如果您认为正在编写的测试跨越了太多类以符合单元测试的资格要求,请使用模拟对象将一个类与系统的其余部分隔离开来。

如果您已经有了自己的设计 - 正如您在Jon LimJap的回答评论中所指出的那样,那么您并没有进行纯TDD,因为TDD是使用单元测试让您的设计出现的过程。

话虽如此,并非所有的商店都允许严格的TDD,而且您手头上已经有了一个设计,所以让我们使用它并进行TDD - 尽管更好的说法是测试驱动编程,但这不是重点,因为这也是我开始TDD的方式。


0

我认为你不应该从TDD开始。说真的,你的规格在哪里?你是否已经就系统的一般/粗略总体设计达成了共识,这可能适用于你的应用程序?我知道TDD和敏捷开发反对大量预先设计,但这并不意味着你在通过实现该设计的方式进行TDD之前不应该首先进行预先设计。


我并没有说要从TDD开始,我是在问你如何践行TDD。我有规范、概念设计和用例。但当编码阶段来临时,你应该从何开始呢?当然,我可以为一些随机的对象编写一堆随机测试,但那样并不能带来任何连贯性。 - Its me

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