23

虽然我已经为我完成的大部分代码编写了单元测试,但我最近才通过 Kent Beck 的示例获得了一份 TDD 副本。我一直对我做出的某些设计决定感到遗憾,因为它们阻止了应用程序“可测试”。我通读了这本书,虽然其中一些看起来很陌生,但我觉得我可以管理它并决定在我当前的项目中尝试它,该项目基本上是一个客户端/服务器系统,两个部分通过它进行通信。USB。一个在小工具上,另一个在主机上。该应用程序使用 Python。

我开始了,很快就陷入了混乱的重写和小测试中,后来我发现这些并没有真正测试任何东西。我把它们中的大部分都扔掉了,现在有了一个工作应用程序,它的测试全部凝结成 2 个。

根据我的经验,我有几个问题想请教。我从 TDD 新手中获得了一些信息:是否有带有测试的示例应用程序来展示如何进行 TDD?但有一些具体的问题我想回答/讨论。

  1. Kent Beck 使用他添加和删除的列表来指导开发过程。你如何制作这样的清单?我最初有一些项目,如“服务器应该启动”,“如果通道不可用,服务器应该中止”等,但它们混合在一起,最后现在,它就像“客户端应该能够连接到服务器”(其中包含服务器启动等)。
  2. 你如何处理重写?我最初选择了基于命名管道的半双工系统,以便我可以在自己的机器上开发应用程序逻辑,然后添加USB通信部分。他们变成了基于套接字的东西,然后从使用原始套接字转移到使用 Python SocketServer 模块。每次事情发生变化时,我发现我不得不重写相当一部分测试,这很烦人。我认为在我的开发过程中,测试将是一个不变的指南。他们只是觉得需要处理更多的代码。
  3. 我需要一个客户端和一个服务器通过通道进行通信以测试任何一方。我可以模拟其中一方来测试另一方,但是整个频道都不会被测试,我担心我会错过。这有损于整个红/绿/重构节奏。这只是缺乏经验还是我做错了什么?
  4. “假装直到成功”给我留下了很多凌乱的代码,后来我花了很多时间重构和清理。这是事情的运作方式吗?
  5. 在会话结束时,我现在让我的客户端和服务器运行大约 3 或 4 个单元测试。我花了大约一周的时间才完成。如果我在代码后使用单元测试,我想我可以在一天内完成。我看不到收益。

我正在寻找使用这种方法完全(或几乎完全)实施大型非平凡项目的人的评论和建议。在我已经运行了一些东西并想要添加一个新功能之后,按照这种方式去做对我来说是有意义的,但是从头开始做这件事似乎很累,而且不值得付出努力。

PS:请让我知道这是否应该是社区维基,我会这样标记它。

更新 0:所有答案都同样有帮助。我选择了我做的那个,因为它最能与我的经历产生共鸣。

更新1:练习练习练习!

4

7 回答 7

10

作为初步评论,TDD 需要实践。当我回顾我开始 TDD 时编写的测试时,我发现了很多问题,就像我查看几年前编写的代码时一样。继续这样做,就像您开始识别好代码和坏代码一样,您的测试也会发生同样的事情 - 要有耐心。

你如何制作这样的清单?我最初有一些项目,例如“服务器应该启动”,“如果通道不可用,服务器应该中止”等,但它们混合在一起,最后现在,它就像“客户端应该能够连接到服务器”

“列表”可能是相当非正式的(贝克的书中就是这种情况),但是当您开始将项目纳入测试时,请尝试将语句写在“[当发生某些事情时]然后[这个条件应该在那]”的格式。这将迫使您更多地考虑您正在验证的是什么,您将如何验证它并直接转换为测试 - 或者如果没有,它应该为您提供有关缺少哪个功能的线索。考虑用例/场景。例如“服务器应该启动”是不清楚的,因为没有人在启动一个动作。

每次事情发生变化时,我发现我不得不重写相当一部分测试,这很烦人。我认为在我的开发过程中,测试将是一个不变的指南。他们只是觉得需要处理更多的代码。

首先,是的,测试更多的是代码,需要维护——编写可维护的测试需要练习。我同意 S. Lott 的观点,如果您需要大量更改测试,那么您可能测试“太深”了。理想情况下,您希望在不太可能更改的公共接口级别进行测试,而不是在可能演变的实现细节级别进行测试。但是练习的一部分是关于提出一个设计,所以你应该预料到其中的一些错误,并且还必须移动/重构你的测试。

我可以模拟其中一方来测试另一方,但是整个频道都不会被测试,我担心我会错过。

不完全确定那个。从它的声音来看,使用模拟是正确的想法:选择一侧,模拟另一侧,并检查每一侧是否工作,假设另一侧已正确实施。一起测试整个系统是集成测试,您也想这样做,但通常不是 TDD 过程的一部分。

“假装直到成功”给我留下了很多凌乱的代码,后来我花了很多时间重构和清理。这是事情的运作方式吗?

在进行 TDD 时,您应该花费大量时间进行重构。另一方面,当你伪造它时,它是暂时的,你的下一步应该是取消伪造它。通常你不应该因为你伪造它而通过多个测试 - 你应该一次专注于一个,并尽快重构它。

如果我在代码后使用单元测试,我想我可以在一天内完成。我看不到收益。

同样,这需要练习,随着时间的推移,你应该会变得更快。另外,有时 TDD 比其他的更有成效,我发现在某些情况下,当我确切地知道我要编写的代码时,写好部分代码然后编写测试会更快。
除了 Beck,我喜欢的一本书是 Roy Osherove 的 The Art of Unit Testing。这不是一本 TDD 书,它是面向 .Net 的,但无论如何你可能都想看看它:一个很好的部分是关于如何编写可维护的测试、测试质量和相关问题。我发现这本书在我写过测试后与我的经历产生了共鸣,有时我很难把它做好……
所以我的建议是,不要把毛巾扔得太快,给它一些时间。您可能还想尝试一些更简单的事情 - 测试与服务器通信相关的事情听起来并不是最容易开始的项目!

于 2010-01-17T05:01:14.460 回答
8
  1. Kent Beck 使用了一个列表......最后,它就像“客户端应该能够连接到服务器”(其中包含服务器启动等)。

通常是一种不好的做法。

对架构的每个单独层进行单独测试是好的。

综合测试往往会掩盖架构问题。

但是,只测试公共功能。不是每个功能。

并且不要花费大量时间来优化您的测试。测试中的冗余不会像在工作应用程序中那样受到伤害。如果事情发生变化并且一个测试有效,但另一个测试中断,那么也许您可以重构您的测试。不是以前。

2. 你如何处理重写?...我发现我必须重写相当一部分测试。

您正在测试的细节级别太低。测试最外层的、公开的、可见的界面。应该不变的部分。

是的,重大的架构更改意味着重大的测试更改。

测试代码是你证明事情有效的方式。它几乎与应用程序本身一样重要。是的,这是更多的代码。是的,你必须管理它。

3. 我需要一个客户端和一个服务器通过通道进行通信来测试任何一方。我可以模拟其中一方来测试另一方,但是整个频道都不会被测试......

有单元测试。带嘲讽。

有集成测试,可以测试整个事情。

不要混淆他们。

您可以使用单元测试工具进行集成测试,但它们是不同的东西。

你需要两者都做。

4. “Fake it until you make it”给我留下了很多乱七八糟的代码,后来我花了很多时间重构和清理。这是事情的运作方式吗?

是的。这正是它的工作原理。从长远来看,有些人发现这比费尽心思尝试预先完成所有设计更有效。有些人不喜欢这样,想要预先做所有的设计;如果你愿意,你可以自由地预先做很多设计。

我发现重构是一件好事,而预先设计太难了。也许是因为我已经编码了将近 40 年,而我的大脑正在磨损。

5. 我看不到收益。

所有真正的天才都发现测试会减慢他们的速度。

在我们有一套完整的测试证明它有效之前,我们其他人无法确定我们的代码是否有效。

如果您不需要证明您的代码有效,则不需要测试。

于 2010-01-14T18:55:00.073 回答
3

问:Kent Beck 使用他添加和删除的列表来指导开发过程。你如何制作这样的清单?我最初有一些项目,如“服务器应该启动”,“如果通道不可用,服务器应该中止”等,但它们混合在一起,最后现在,它就像“客户端应该能够连接到服务器”(其中包含服务器启动等)。

我首先选择我可能检查的任何东西。在您的示例中,您选择了“服务器启动”。

服务器启动

现在我寻找任何我想写的更简单的测试。变化较少,活动部件较少的东西。例如,我可能会考虑“正确配置服务器”。

正确配置服务器
服务器启动

但是,实际上,“服务器启动”取决于“正确配置的服务器”,因此我明确了该链接。

正确配置服务器
如果配置正确,服务器将启动

现在我寻找变化。我问:“会出什么问题?” 我可能会错误地配置服务器。有多少种不同的方式很重要?每一个都进行测试。即使我正确配置了服务器,它怎么可能无法启动?每一个案例都会进行测试。

问:你如何处理重写?我最初选择了基于命名管道的半双工系统,以便我可以在自己的机器上开发应用程序逻辑,然后添加USB通信部分。他们变成了基于套接字的东西,然后从使用原始套接字转移到使用 Python SocketServer 模块。每次事情发生变化时,我发现我不得不重写相当一部分测试,这很烦人。我认为在我的开发过程中,测试将是一个不变的指南。他们只是觉得需要处理更多的代码。

当我改变行为时,我发现改变测试是合理的,甚至先改变它们!但是,如果我必须更改不直接检查我正在更改的行为的测试,这表明我的测试依赖于太多不同的行为。这些是集成测试,我认为这是一个骗局。(谷歌“集成测试是个骗局”)

问:我需要一个客户端和一个服务器通过通道进行通信以测试任何一方。我可以模拟其中一方来测试另一方,但是整个频道都不会被测试,我担心我会错过。这有损于整个红/绿/重构节奏。这只是缺乏经验还是我做错了什么?

如果我构建了一个客户端、一个服务器和一个通道,那么我会尝试单独检查每一个。我从客户端开始,当我试驾它时,我决定服务器和通道的行为方式。然后我实现每个通道和服务器以匹配我需要的行为。检查客户端时,我将通道存根;检查服务器时,我模拟了频道;检查频道时,我会存根并模拟客户端和服务器。我希望这对你有意义,因为我必须对这个客户端、服务器和通道的性质做出一些严肃的假设。

问:“假装直到你成功”给我留下了很多凌乱的代码,后来我花了很多时间重构和清理。这是事情的运作方式吗?

如果你在清理之前让你的“伪造”代码变得非常混乱,那么你可能花了太长时间伪造它。也就是说,我发现即使我最终使用 TDD 清理了更多代码,但整体节奏感觉要好得多。这来自实践。

问:在会话结束时,我现在让我的客户端和服务器运行大约 3 或 4 个单元测试。我花了大约一周的时间才完成。如果我在代码后使用单元测试,我想我可以在一天内完成。我看不到收益。

我不得不说,除非您的客户端和服务器非常非常简单,否则您需要分别进行 3 或 4 次以上的测试才能彻底检查它们。我猜你的测试一次检查(或至少执行)许多不同的行为,这可能解释了你编写它们所花费的努力。

另外,不要衡量学习曲线。我的第一次真正的 TDD 体验包括在 9 个 14 小时的工作日内重写 3 个月的工作。我有 125 个测试需要 12 分钟才能运行。我不知道自己在做什么,感觉很慢,但感觉很稳定,结果很棒。我基本上在 3 周内重写了最初需要 3 个月才能出错的内容。如果我现在写,我可能会在 3-5 天内完成。区别?我的测试套件将有 500 个测试,需要 1-2 秒才能运行。这是随着练习而来的。

于 2010-01-20T08:10:51.827 回答
2

作为一名新手程序员,我发现测试驱动开发的棘手之处在于测试应该放在首位的想法。

对于新手来说,这实际上是不正确的。设计是第一位的。(接口,对象和类,方法,任何适合你的语言的东西。)然后你写你的测试。然后你编写真正做事的代码。

我已经有一段时间没有看这本书了,但 Beck 的写作似乎就像代码的设计只是在你的脑海中无意识地发生了一样。对于有经验的程序员来说,这可能是真的,但对于像我这样的菜鸟来说,呃。

我发现Code Complete的前几章对于思考设计非常有用。他们强调了这样一个事实,即您的设计很可能会发生变化,即使您处于实施的基本级别。发生这种情况时,您可能不得不重新编写测试,因为它们基于与您的设计相同的假设。

编码很难。一起去购物吧。

于 2010-02-10T15:23:36.187 回答
1

您如何制作这样一个列表以添加和删除以指导开发过程?我最初有一些项目,例如“服务器应该启动”,“如果通道不可用,服务器应该中止”

TDD TODO 列表中的项目比这更细粒度,它们旨在仅测试一种方法的一种行为,例如:

  • 测试成功的客户端连接
  • 测试客户端连接错误类型 1
  • 测试客户端连接错误类型 2
  • 测试成功的客户端通信
  • 未连接时测试客户端通信失败

你可以为你给出的每个例子建立一个测试列表(正面和负面)。此外,在进行单元测试时,您不会在服务器和客户端之间建立任何连接。您只是单独调用方法,...这回答了问题 3。

你如何处理重写?

如果单元测试测试的是行为而不是实现,那么就不必重写它们。如果单元测试代码确实创建了一个命名管道来与生产代码通信,那么显然在从管道切换到套接字时必须修改测试。单元测试应远离文件系统、网络、数据库等外部资源,因为它们很慢,可能不可用……请参阅这些单元测试规则

这意味着最低级别的功能没有进行单元测试,它们将通过集成测试进行测试,整个系统都是端到端测试的。

于 2010-01-15T17:57:23.343 回答
1

命名管道被放在正确的接口后面,改变该接口的实现方式(从命名管道到套接字再到另一个套接字库)只会影响实现该接口的组件的测试。因此,更多/不同地切割东西会有所帮助......套接字后面的接口可能会演变成。

我可能在 6 个月前开始做 TDD?我还在学习自己。我可以说随着时间的推移,我的测试和代码已经变得更好了,所以请继续努力。我也非常推荐这本书 XUnit Design Patterns。

于 2010-01-14T18:55:09.603 回答
1

对于第一点,请参阅我不久前提出的与您的第一点有关的问题

我将提供一些全球性建议,而不是依次处理其他问题。实践。我花了很长时间和一些“狡猾”的项目(尽管是个人的)来实际获得 TDD。只是谷歌有更多令人信服的理由说明为什么 TDD 如此出色。

尽管测试推动了我的代码设计,但我仍然得到一块白板并草草写出一些设计。由此,至少您对自己要做什么有了一些了解。然后我生成我认为我需要的每个夹具的测试列表。一旦你开始工作,更多的特性和测试就会被添加到列表中。

从您的问题中脱颖而出的一件事是再次重写测试的行为。这听起来像是在进行行为测试,而不是状态测试。换句话说,测试听起来与您的代码紧密相关。因此,不影响输出的简单更改会破坏一些测试。单元测试(至少是好的单元测试)也是一种需要掌握的技能。

我非常推荐Google 测试博客,因为那里的一些文章使我对 TDD 项目的测试变得更好。

于 2010-01-14T18:42:42.063 回答