今天阅读了一篇文章,标题叫做Dream Code First,我译作优先想像代码。整篇文章的结构良好,链接:Dream Code First
首先作者给出两种开发工作流程(workflow):
作者认为1是所谓的test-last workflow,而2是所谓的test-first workflow。很显然这是一篇推荐测试驱动开发(Test-Driven Development / TDD)的文章。
文中观点表示第一种开发流程中,最先完成的代码实现限制了测试和重构的进行(The design is accidental. The tests are sloppy, and inhibit refactoring);而第二种开发流程中,最先编写测试代码让程序员更专注编写实现代码,只需要考虑代码质量(You’re only concerned with the quality of the code),同时使重构变得简单(Once the test passes, refactoring is easy)。所以建议测试代码先行,在编写测试代码的同时想像完美实现(You dream up the perfect code, and then write it down),这也就是标题所说的Dream Code First。
测试驱动开发理念的文章其实已经看得很多了,首先应该明确一个前提:对测试驱动开发来说,单元测试是必须的。那么广泛地来说,单元测试是不是必须的呢?我认为不一定。测试分为很多种类或者说阶段,单元测试、集成测试、冒烟测试、系统测试等。在软件出厂之前必然会经过一系列测试来保证软件的正确性、稳定性、健壮性、性能等等,很多公司包括我呆过的两个团队都并没有单元测试这个环节。单元测试的特殊性在于,这是一项代码级的测试,可以反复执行、可以复用;同时这是由开发人员而不是测试人员来保证的一项测试流程。这增加了开发人员的代码量、工作时长、学习成本,也意味着同一个项目需要更多人力投入,更多管理成本。所以这就是为什么很多公司没有单元测试。
那么引入测试驱动开发的目的是什么?我认为是更高的代码质量。文章中也说test-first workflow会使程序员更多考虑代码质量,使重构变得简单。重构的好处不必赘言,只是和单元测试一样,重构也需要额外的工时投入。所以最后所有矛头都指向了代码质量。代码是一家软件公司的资产,那么代码质量自然是非常重要的。
综上所述,我认为引入测试驱动开发还是利大于弊。身为程序员,如果使用过leetcode这样的oj系统,自然会理解测试驱动开发的妙处。只是各类工具都有其适用人群,我认为“大型、稳定的公司或团队”才适用。大,说明开发人员充足;稳定,说明业务并不处于需要极速扩张的阶段。这两点都说明开发工时均不是瓶颈点,这时候才更应该关注于产品或者说代码本身的质量上。
程序员开源交流QQ群 792272915
————————————————————
附原文:
Dream Code First
Let’s consider two software development workflows. Their outcomes differ in many ways, but in this article I just want to focus on code quality.
Workflow #1: Straight to the implementation
Look around the codebase for existing places that will need to be changed.
Change and add code until the new functionality works, doing manual testing.
Think about refactoring, but decide against it, because it’s working, and you’ll have to manually test everything again if you make changes.
Write some tests to cover the new code.
Refactor some of the code, without breaking the tests you just wrote.
This workflow does not involve design. Changes are made haphazardly until the implementation works. The resulting design is an afterthought — just a combination of all the changes.
Next, the implementation is locked in place by tests. The tests aren’t written very well, because they were also an afterthought. They are written in the same way as the implementation: haphazardly adding and changing code until they pass.
This might limit your ability to refactor. The tests are often tightly coupled to implementation details. The implementation is right there in front of you, so when you write the tests, of course they’re going to match up. You don’t want to touch the tests because they are supposed to remain green and unchanged during refactoring. And besides, why write tests if you’re immediately going to break them? That seems like a waste of time.
This is a common test-last workflow. The implementation is the first code that worked (plus some of the code that didn’t). The design is accidental. The tests are sloppy, and inhibit refactoring.
The repeated application of this workflow will turn a codebase into spaghetti.
Workflow #2: Starting with tests
Open or create a test file
Imagine what a good quality implementation might look like
Write a test for this imaginary implementation
Iterate on the design, adapting it to best fit your requirements
Run the test, fix the error, and repeat until the test passes
Refactor the implementation until it meets your desired standard of quality
Repeat until all of the functionality is implemented
Design is the first thing that happens in this workflow. You dream up the perfect code, and then write it down. Then you improve the code by experimenting, trying to find better approaches.
The test reads nicely, because it only contains dream code. You’re not concerned whether it will pass, because you know that it won’t. You’re only concerned with the quality of the code.
Next, the dream is methodically turned into reality. The test failure message tells you exactly what you need to implement next. It doesn’t matter how sloppy the implementation is at this point, because it can be cleaned up after the test passes. This gives you laser focus.
Once the test passes, refactoring is easy. The test has locked in the dream interface, not the implementation, because the implementation didn’t even exist when the test was written. This gives you the freedom to change any of the implementation details without breaking the test.
This is a test-first workflow. The tests read well, you have a quality design that fits your requirements, and the implementation is as clean as you want it to be.
The repeated application of this workflow helps to maintain a high level of quality in a codebase.
Well, actually…
I can design code properly without writing the tests first. My code isn’t spaghetti.
This is the “grandma smoked a packet of cigarettes every day and she lived to be 102” argument. I’m sure it’s true, but did smoking cause grandma to live longer? No. Does it prove that smoking is safe? No. Grandma lived to be 102 in spite of smoking, not because of it. Imagine how much money she could have saved, how much better her health could have been, and how long she might have lived if she didn’t smoke.
A workflow with deliberate design and ease of refactoring will produce better code than a workflow with accidental design and difficulties refactoring. You might be able to use your experience and self-discipline to overcome those problems, but if the software development industry has taught us anything, it’s that relying on the self-discipline of software developers is a bad idea. It’s just human nature.
I’ve seen test-first code that was terrible.
Test-first doesn’t guarantee that the design is perfect, or even good. Design is an advanced skill, and newer developers will produce suboptimal designs regardless of the testing workflow. Making them write tests last won’t improve the code — it will make the code worse.
With test-first, at least the developer has a chance to exercise their design skills. Prompting people to think about their requirements before writing the implementation is a great way to improve.
I write tests last and I don’t have any problems refactoring.
Which workflow is more likely to create tests that are coupled to implementation details:
one where the implementation is written first, and then tests are written afterwards?
Or one where the implementation doesn’t even exist at the point when the tests are written?
It seems like common sense to me.
Again, it comes down to human nature. You can use experience and self-discipline to write better tests, or you can follow a process that produces better tests naturally.
Automated testing doesn’t suit every situation.
If automated testing doesn’t work well for your situation, then testing first vs last is a moot point. But if you are going to write a test, you might as well write it first.
Sometimes you can’t write the test first because you have no idea what the code will look like.
Often we know roughly what we want, but don’t know how to write it. To put it another way, we have some idea about the interface, but not the implementation. TDD is a process for discovering the implementation, so it works perfectly in this situation.
If you don’t know what you want, then you can’t write a test for it. In this situation, you might want to do a “spike”, which is experimentation without any tests. Make a mess, and do whatever you have to, to learn what kind of interface you need. Once you have an idea about the interface, consider throwing away all the code from the spike, and starting from scratch with a test.
Conclusion
In terms of design quality, test quality, and implementation quality, I think that test-first wins in all three categories. It doesn’t guarantee quality code, it’s just better than test-last, on average.
When you dream up code that doesn’t exist yet, you dream up good code. Imaginary code has unlimited possibilities, from which you can pick the best choices. Nobody chooses to write bad code on purpose, it’s just something that happens accidentally. One way to accidentally write bad code is to jump straight into the implementation, without considering the design, and then lock it in with tests.
When you consider how the effects accumulate over months and years, I think most of us would prefer to accumulate the dreamy test-first code.
Dream code first, before you start implementing it.