TDD - 初学者问题和绊脚石
虽然我已经为自己完成的大部分代码编写了单元测试,但我最近才拿到 Kent Beck 编写的 TDD 示例副本。我一直对自己做出的某些设计决定感到遗憾,因为它们阻止了应用程序的“可测试”。我通读了这本书,虽然其中一些内容看起来很陌生,但我觉得我可以管理它,并决定在我当前的项目中尝试一下,该项目基本上是一个客户端/服务器系统,两个部分通过该系统进行通信。 USB。一个在小工具上,另一个在主机上。该应用程序是用 Python 编写的。
我开始了,很快就陷入了一堆混乱的重写和小测试中,后来我发现这些测试并没有真正测试任何东西。我扔掉了其中的大部分,现在有了一个可以运行的应用程序,其测试全部凝结为 2 个。
根据我的经验,我有几个问题想问。我从 TDD 新手:是否有带有测试的示例应用程序来展示如何进行 TDD?,但有一些具体问题,我希望得到答案/讨论。
- Kent Beck 使用一个他添加和删除的列表来指导开发过程。你如何列出这样的清单?我最初有一些项目,例如“服务器应该启动”,“如果通道不可用,服务器应该中止”等,但它们混合在一起,最后现在,它只是“客户端应该能够连接到服务器”(其中包含服务器启动等)。
- 你如何处理重写?我最初选择了基于命名管道的半双工系统,以便我可以在自己的机器上开发应用程序逻辑,然后添加USB通信部分。他们转变为基于套接字的东西,然后从使用原始套接字转变为使用 Python SocketServer 模块。每次事情发生变化时,我发现我必须重写相当一部分的测试,这很烦人。我认为测试在我的开发过程中将是一个不变的指南。他们只是觉得需要处理更多代码。
- 我需要一个客户端和一个服务器通过通道进行通信以测试任一端。我可以模拟一侧来测试另一侧,但是整个通道不会被测试,我担心我会错过这一点。这破坏了整个红/绿/重构的节奏。这只是缺乏经验还是我做错了什么?
- “假装直到成功”给我留下了很多混乱的代码,后来我花了很多时间来重构和清理它们。这是事情的运作方式吗?
- 在会话结束时,我现在让我的客户端和服务器运行大约 3 或 4 个单元测试。我花了大约一周的时间才完成。我想如果我在代码之后使用单元测试的话我可以在一天内完成。我看不到收获。
我正在寻找完全(或几乎完全)使用这种方法实现大型非平凡项目的人们的评论和建议。对我来说,遵循之后的方式是有意义的,我已经运行了一些东西,并且想要添加新功能,但从头开始似乎很烦人,而且不值得付出努力。
PS:请告诉我这是否应该是社区 wiki,我会这样标记它。
更新 0:所有答案都同样有帮助。我选择了我所做的一个,因为它与我的经历最有共鸣。
更新1:练习练习练习!
While I've written unit tests for most of the code I've done, I only recently got my hands on a copy of TDD by example by Kent Beck. I have always regretted certain design decisions I made since they prevented the application from being 'testable'. I read through the book and while some of it looks alien, I felt that I could manage it and decided to try it out on my current project which is basically a client/server system where the two pieces communicate via. USB. One on the gadget and the other on the host. The application is in Python.
I started off and very soon got entangled in a mess of rewrites and tiny tests which I later figured didn't really test anything. I threw away most of them and and now have a working application for which the tests have all coagulated into just 2.
Based on my experiences, I have a few questions which I'd like to ask. I gained some information from New to TDD: Are there sample applications with tests to show how to do TDD? but have some specific questions which I'd like answers to/discussion on.
- Kent Beck uses a list which he adds to and strikes out from to guide the development process. How do you make such a list? I initially had a few items like "server should start up", "server should abort if channel is not available" etc. but they got mixed and finally now, it's just something like "client should be able to connect to server" (which subsumed server startup etc.).
- How do you handle rewrites? I initially selected a half duplex system based on named pipes so that I could develop the application logic on my own machine and then later add the USB communication part. It them moved to become a socket based thing and then moved from using raw sockets to using the Python SocketServer module. Each time things changed, I found that I had to rewrite considerable parts of the tests which was annoying. I'd figured that the tests would be a somewhat invariable guide during my development. They just felt like more code to handle.
- I needed a client and a server to communicate through the channel to test either side. I could mock one of the sides to test the other but then the whole channel wouldn't be tested and I worry that I'd miss that. This detracted from the whole red/green/refactor rhythm. Is this just lack of experience or am I doing something wrong?
- The "Fake it till you make it" left me with a lot of messy code that I later spent a lot of time to refactor and clean up. Is this the way things work?
- At the end of the session, I now have my client and server running with around 3 or 4 unit tests. It took me around a week to do it. I think I could have done it in a day if I were using the unit tests after code way. I fail to see the gain.
I'm looking for comments and advice from people who have implemented large non trivial projects completely (or almost completely) using this methodology. It makes sense to me to follow the way after I have something already running and want to add a new feature but doing it from scratch seems to tiresome and not worth the effort.
P.S. : Please let me know if this should be community wiki and I'll mark it like that.
Update 0 : All the answers were equally helpful. I picked the one I did because it resonated with my experiences the most.
Update 1: Practice Practice Practice!
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(7)
作为初步评论,TDD 需要实践。当我回顾开始 TDD 时编写的测试时,我发现了很多问题,就像我查看几年前编写的代码一样。继续这样做,就像您开始识别好代码和坏代码一样,您的测试也会发生同样的事情 - 要有耐心。
“列表”可能相当非正式(贝克的书中就是这种情况),但是当您开始将这些项目放入测试中时,请尝试将语句写在“[当发生这种情况]然后[这个条件在那个]”格式上应该是正确的。这将迫使你更多地思考你正在验证的是什么,你将如何验证它并直接转化为测试 - 或者如果不是,它应该给你一个线索例如,缺少哪一项功能是不清楚的,因为没有人启动操作。
首先,是的,测试是更多的代码,并且需要维护——编写可维护的测试需要练习。我同意 S. Lott 的观点,如果您需要大量更改测试,那么您可能测试“太深”了。理想情况下,您希望在不太可能改变的公共接口级别进行测试,而不是在可能演变的实现细节级别进行测试。但练习的一部分是提出设计,因此您应该预料到会出现一些错误,并且还必须移动/重构您的测试。
对此不太确定。从听起来来看,使用模拟是正确的想法:选择一侧,模拟另一侧,并检查每一侧是否正常工作,假设另一侧已正确实现。一起测试整个系统就是集成测试,您也想要这样做,但通常不是 TDD 过程的一部分。
在进行 TDD 时,您应该花费大量时间进行重构。另一方面,当你伪造它时,它是暂时的,你下一步应该立即取消伪造它。通常,您不应该因为伪造而通过多个测试 - 您应该一次专注于一个部分,并尽快重构它。
再次强调,这需要练习,随着时间的推移,你应该会变得更快。另外,有时 TDD 比其他方法更富有成效,我发现在某些情况下,当我确切地知道我要编写的代码时,编写一部分代码,然后编写测试会更快。
除了 Beck 之外,我喜欢的一本书是 Roy Osherove 写的《单元测试的艺术》。这不是一本 TDD 书,它是面向 .Net 的,但您可能还是想看一下:其中一个很好的部分是关于如何编写可维护的测试、测试质量和相关问题。我发现这本书与我在进行书面测试后的经验产生共鸣,有时很难正确地做到这一点......
所以我的建议是,不要太快认输,给一些时间。您可能还想尝试一些更简单的事情 - 测试服务器通信相关的事情听起来不像是最容易开始的项目!
As a preliminary comment, TDD takes practice. When I look back at the tests I wrote when I began TDD, I see lots of issues, just like when I look at code I wrote a few year ago. Keep doing it, and just like you begin to recognize good code from bad, the same things will happen with your tests - with patience.
"The list" can be rather informal (that's the case in Beck's book) but when you move into making the items into tests, try to write the statements in a "[When something happens to this] then [this condition should be true on that]" format. This will force you to think more about what it is you are verifying, how you would verify it and translates directly into tests - or if it doesn't it should give you a clue about which piece of functionality is missing. Think use case / scenario. For instance "server should start up" is unclear, because nobody is initiating an action.
First, yes, tests are more code, and requires maintenance - and writing maintainable tests takes practice. I agree with S. Lott, if you need to change your tests a lot, you are probably testing "too deep". Ideally you want to test at the level of the public interface, which is not likely to change, and not at the level of the implementation detail, which could evolve. But part of the exercise is about coming up with a design, so you should expect to get some of it wrong and have to move/refactor your tests as well.
Not totally sure about that one. From the sound of it, using a mock was the right idea: take one side, mock the other one, and check that each side works, assuming the other one is implemented properly. Testing the whole system together is integration testing, which you also want to do, but is typically not part of the TDD process.
You should spend a lot of time refactoring while doing TDD. On the other hand, when you fake it, it's temporary, and your immediate next step should be to un-fake it. Typically you shouldn't have multiple tests passing because you faked it - you should be focusing on one piece at a time, and work on refactoring it ASAP.
Again, it takes practice, and you should get faster over time. Also, sometimes TDD is more fruitful than others, I find that in some situations, when I know exactly the code I want to write, it's just faster to write a good part of the code, and then write tests.
Besides Beck, one book I enjoyed is The Art of Unit Testing, by Roy Osherove. It's not a TDD book, and it is .Net-oriented, but you might want to give it a look anyways: a good part is about how to write maintainable tests, tests quality and related questions. I found that the book resonated with my experience after having written tests and sometimes struggled to do it right...
So my advice is, don't throw the towel too fast, and give it some time. You might also want to give it a shot on something easier - testing server communication related things doesn't sound like the easiest project to start with!
通常是一种不好的做法。
对架构的每个单独层进行单独测试都是很好的。
综合测试往往会掩盖架构问题。
但是,仅测试公共功能。不是每个功能。
并且不要投入大量时间来优化测试。测试中的冗余不会像工作应用程序中那样造成太大影响。如果情况发生变化并且一个测试有效,但另一个测试失败,也许您可以重构您的测试。以前没有。
您的测试细节水平太低。测试最外面的、公共的、可见的接口。应该不变的部分。
是
的,重大的架构变化意味着重大的测试变化。
。
测试代码是您证明事情有效的方式 它几乎与应用程序本身一样重要。是的,代码更多。是的,你必须管理它。
有单元测试。带有嘲笑。
有集成测试,可以测试整个事情。
不要混淆他们。
您可以使用单元测试工具来进行集成测试,但它们是不同的东西。
你需要两者都做。
是的。这就是它的工作原理。从长远来看,有些人发现这比绞尽脑汁试图预先完成所有设计更有效。有些人不喜欢这样,想要预先完成所有设计;如果您愿意,您可以自由地预先进行大量设计。
我发现重构是一件好事,但预先设计太难了。也许是因为我已经编码了近 40 年,我的大脑已经磨损了。
所有真正的天才都发现测试会减慢他们的速度。
在我们拥有一套完整的测试来证明我们的代码有效之前,我们其他人无法确定我们的代码是否有效。
如果您不需要证明您的代码可以工作,那么您就不需要测试。
Often a bad practice.
Separate tests for each separate layer of the architecture are good.
Consolidated tests tend to obscure architectural issues.
However, only test the public functions. Not every function.
And don't invest a lot of time optimizing your testing. Redundancy in the tests doesn't hurt as much as it does in the working application. If things change and one test works, but another test breaks, perhaps then you can refactor your tests. Not before.
You're testing at too low a level of detail. Test the outermost, public, visible interface. The part that's supposed to be unchanging.
And
Yes, significant architectural change means significant testing change.
And
The test code is how you prove things work. It is almost as important as the application itself. Yes, it's more code. Yes, you must manage it.
There are unit tests. With mocks.
There are integration tests, which test the whole thing.
Don't confuse them.
You can use unit test tools to do integration tests, but they're different things.
And you need to do both.
Yes. That's exactly how it works. In the long run, some people find this more effective than straining their brains trying to do all the design up front. Some people don't like this and want to do all the design up front; you're free to do a lot of design up front if you want to.
I've found that refactoring is a good thing and design up front is too hard. Maybe it's because I've been coding for almost 40 years and my brain is wearing out.
All the true geniuses find that testing slows them down.
The rest of us can't be sure our code works until we have a complete set of tests that prove that it works.
If you don't need proof that your code works, you don't need testing.
我首先选择我可能检查的任何内容。在您的示例中,您选择了“服务器启动”。
现在我寻找我可能想要编写的任何更简单的测试。变化较少、活动部件较少的东西。例如,我可能会考虑“正确配置服务器”。
不过,实际上,“服务器启动”取决于“正确配置的服务器”,所以我明确了该链接。
现在我寻找变化。我问:“会出什么问题吗?”我可能错误地配置了服务器。有多少种不同的方式很重要?其中每一个都进行测试。即使我配置正确,服务器仍然无法启动,这是怎么回事?每个案例都需要进行测试。
当我改变行为时,我发现改变测试是合理的,甚至首先改变它们!但是,如果我必须更改不直接检查我正在更改的行为的测试,则表明我的测试依赖于太多不同的行为。这些是集成测试,我认为这是一个骗局。 (谷歌“集成测试是一个骗局”)
如果我构建一个客户端、一个服务器和一个通道,那么我会尝试单独检查每一个。我从客户端开始,当我测试它时,我决定服务器和通道需要如何运行。然后我实现通道和服务器以匹配我需要的行为。检查客户端时,我对通道进行存根;检查服务器时,我模拟频道;检查通道时,我对客户端和服务器进行存根和模拟。我希望这对您有意义,因为我必须对该客户端、服务器和通道的性质做出一些认真的假设。
如果你在清理之前让你的“伪造”代码变得非常混乱,那么你可能花了太长时间来伪造它。也就是说,我发现尽管我最终使用 TDD 清理了更多代码,但整体节奏感觉好多了。这是来自实践的。
我不得不说,除非你的客户端和服务器非常非常简单,否则你需要分别进行超过3或4次测试才能彻底检查它们。我猜测您的测试会同时检查(或至少执行)许多不同的行为,这可能会解释您编写它们所花费的精力。
另外,不要测量学习曲线。我的第一次真正的 TDD 体验是在每天 9 到 14 小时内重写 3 个月的工作成果。我进行了 125 个测试,运行时间为 12 分钟。我不知道自己在做什么,感觉很慢,但感觉很稳定,而且结果很棒。我基本上在三周内重写了原本花了三个月才出错的内容。如果我现在写的话,大概3-5天就能写完。区别?我的测试套件将包含 500 个测试,运行时间为 1-2 秒。这是通过练习而来的。
I start by picking anything I might check. In your example, you chose "server starts".
Now I look for any simpler test I might want to write. Something with less variation, and fewer moving parts. I might consider "configured server correctly", for example.
Really, though, "server starts" depends on "configured server correctly", so I make that link clear.
Now I look for variations. I ask, "What could go wrong?" I could configure the server incorrectly. How many different ways that matter? Each of those makes a test. How might the server not start even though I configured it correctly? Each case of that makes a test.
When I change behavior, I find it reasonable to change the tests, and even to change them first! If I have to change tests that don't directly check the behavior I'm in the process of changing, though, that's a sign that my tests depend on too many different behaviors. Those are integration tests, which I think are a scam. (Google "Integration tests are a scam")
If I build a client, a server, and a channel, then I try to check each in isolation. I start with the client, and when I test-drive it, I decide how the server and channel need to behave. Then I implement the channel and server each to match the behavior I need. When checking the client, I stub the channel; when checking the server, I mock the channel; when checking the channel, I stub and mock both client and server. I hope this makes sense to you, since I have to make some serious assumptions about the nature of this client, server, and channel.
If you let your "fake it" code get very messy before cleaning it up, then you might have spent too long faking it. That said, I find that even though I end up cleaning up more code with TDD, the overall rhythm feels much better. This comes from practice.
I have to say that unless your client and server are very, very simple, you need more than 3 or 4 tests each to check them thoroughly. I will guess that your tests check (or at least execute) a number of different behaviors at once, and that might account for the effort it took you to write them.
Also, don't measure the learning curve. My first real TDD experience consisted of re-writing 3 months' worth of work in 9, 14-hour days. I had 125 tests that took 12 minutes to run. I had no idea what I was doing, and it felt slow, but it felt steady, and the results were fantastic. I essentially re-wrote in 3 weeks what originally took 3 months to get wrong. If I wrote it now, I could probably do it in 3-5 days. The difference? My test suite would have 500 tests that take 1-2 seconds to run. That came with practice.
作为一名新手程序员,我发现测试驱动开发的棘手之处在于测试应该放在第一位。
对于新手来说,事实并非如此。设计是第一位的。 (接口、对象和类、方法,任何适合您的语言的内容。)然后您可以为此编写测试。然后你编写实际执行操作的代码。
我已经有一段时间没有看这本书了,但贝克写的似乎代码的设计就好像在你的脑海中无意识地发生一样。对于经验丰富的程序员来说,这可能是正确的,但对于像我这样的菜鸟来说,嗯嗯。
我发现Code Complete 的前几章对于思考设计非常有用。他们强调这样一个事实,即您的设计很可能会发生变化,即使您已经陷入了具体的实现层面。当发生这种情况时,您很可能必须重新编写测试,因为它们基于与您的设计相同的假设。
编码很难。我们去购物吧。
As a novice programmer, the thing I found tricky about test-driven development was the idea that testing should come first.
To the novice, that’s not actually true. Design comes first. (Interfaces, objects and classes, methods, whatever’s appropriate to your language.) Then you write your tests to that. Then you write the code that actually does stuff.
It’s been a while since I looked at the book, but Beck seems to write as if the design of the code just sort of happens unconsciously in your head. For experienced programmers, that may be true, but for noobs like me, nuh-uh.
I found the first few chapters of Code Complete really useful for thinking about design. They emphasise the fact that your design may well change, even once you’re down at the nitty gritty level of implementation. When that happens, you may well have to re-write your tests, because they were based on the same assumptions as your design.
Coding is hard. Let’s go shopping.
对于第一点,请参阅问题我不久前问过有关你的第一点的问题。
我不会依次处理其他问题,而是提供一些全局建议。实践。我花了很长时间和一些“狡猾”的项目(尽管是个人的)才真正获得 TDD。谷歌一下就能找到更多令人信服的理由来解释为什么 TDD 如此优秀。
尽管测试驱动了我的代码设计,但我仍然拿到白板并潦草地写下一些设计。由此,至少您对自己应该做什么有一些了解。然后我生成我认为需要的每个夹具的测试列表。一旦您开始工作,更多功能和测试就会添加到列表中。
从您的问题中脱颖而出的一件事是再次重写测试的行为。这听起来像是你在进行行为测试,而不是状态测试。换句话说,测试听起来与您的代码联系得太紧密了。因此,不影响输出的简单更改将破坏某些测试。单元测试(至少是良好的单元测试)也是一项需要掌握的技能。
我强烈推荐 Google 测试博客,因为其中的一些文章使我对 TDD 项目的测试变得更好。
For point one, see a question I asked a while back relating to your first point.
Rather than handle the other points in turn, I'll offer some global advice. Practice. It took me a good while and a few 'dodgy' projects (personal though) to actual get TDD. Just Google for much more compelling reasons on why TDD is so good.
Despite the tests driving the design of my code, I still get a whiteboard and scribble out some design. From this, at least you have some idea of what you are meant to be doing. Then I produce the list of tests per fixture that I think I need. Once you start working, more features and tests get added to the list.
One thing that stood out from your question is the act of rewriting your tests again. This sounds like you are carrying out behavioural tests, rather than state. In other words, the tests sound too closely tied to your code. Thus, a simple change that doesn't effect the output will break some tests. Unit testing (at least good unit testing) too, is a skill to master.
I recommend the Google Testing Blog quite heavily because some of the articles on there made my testing for TDD projects much better.
命名管道被放在正确的接口后面,改变该接口的实现方式(从命名管道到套接字到另一个套接字库)应该只会影响实现该接口的组件的测试。因此,以更多/不同的方式切割事物会有所帮助......套接字后面的接口可能会演变为。
我大概 6 个月前开始做 TDD?我自己还在学习中。我可以说,随着时间的推移,我的测试和代码已经变得更好了,所以继续保持吧。我也强烈推荐《XUnit 设计模式》这本书。
The the named pipes were put behind the right interface, changing how that interface is implemented (from named pipes to sockets to another sockets library) should only impact tests for the component that implements that interface. So cutting things up more/differently would have helped... That interface the sockets are behind will likely evolve to.
I started doing TDD maybe 6 months ago? I am still learning myself. I can say over time my tests and code have gotten much better, so keep it up. I really recommend the book XUnit Design Patterns as well.
细粒度,它们旨在仅测试一种方法的一种行为,例如:
您可以为您提供的每个示例构建一个测试列表(正面和负面),此外,在单元测试时,您无需在服务器和客户端之间建立任何连接。 ..这回答了问题3。
如果单元测试测试行为而不是实现,则不必重写它们。如果单元测试代码确实创建了一个命名管道来与生产代码进行通信,那么显然在从管道切换到套接字时必须修改测试。
单元测试应远离文件系统、网络、数据库等外部资源,因为它们速度很慢,可能不可用......请参阅这些 单元测试规则。
这意味着最低级别的功能没有经过单元测试,它们将通过集成测试进行测试,其中整个系统进行端到端测试。
Items in TDD TODO lists are finer grained than that, they aim at testing one behavior of one method only, for instance:
You could build a list of tests (positive and negative) for every example you gave. Moreover, when unit testing you do not establish any connection between the server and the client. You just invoke methods in isolation, ... This answers question 3.
If the unit test tests behavior and not implementation, then they do not have to be rewritten. If unit test code really creates a named pipe to communicate with production code and, then obviously the tests have to be modified when switching from pipe to socket.
Unit tests shall stay away from external resources such as filesystems, networks, databases because they are slow, can be unavailable ... see these Unit Testing rules.
This implies the lowest level function are not unit tested, they will be tested with integration tests, where the whole system is tested end-to-end.