Ask HN: Do you write tests before the implementation?
I mean how many of you stick with this test driven development practice consistently? Can you describe the practical benefit? Do you happen to rewrite the tests completely while doing the implementation? When does this approach work for you and when did it fail you?
328 comments
[ 4.4 ms ] story [ 329 ms ] threadOnce, I started with tests, but I had to rip up a lot along the way.
It is helpful to ensure testability early on. It might be easier for some devs to figure it out by actually coding up some tests early.
I won’t argue against anyone who is actually productive using hard-core TDD.
While doing this I also found one more benefit, at least for my use case. The backend for user login was simple when I started, but it started growing in a few weeks. Writing test cases saved me from manually logging in with each use case, testing some functionality, then logging out and repeating with other use cases.
Not sure if it is a practical benefit or not, but writing test cases initially also helped me rewrite the way I was configuring Redis for a custom module so that the module can be tested better.
My only issue is that it takes time, and selling higher-ups this was kind of difficult.
Can you describe the practical benefit? Say, a change is executed on one section of the (enterprise level)application. You missed addressing an associated section. This is easily identified as your test will FAIL. When the number of feature increases, the complexity of the application increases. Tests guide you. They help you to ship faster, as you don't need to manually test the whole application again. In manual testing, there are chances of missing out few cases. If it's automated, such cases are all executed. Moreover, in TDD - you only write code which is necessary to complete the feature. Personally, tests act as a (guided)document for the application.
Do you happen to rewrite the tests completely while doing the implementation? Yes, if the current tests doesn't align with the requirements.
When does this approach work for you and when did it fail you? WORK - I wouldn't call it a silver bullet. But I am really grateful/happy to be a developer following TDD. As the codebase increases, when new developers are brought in - TESTS is one of the metrics which helps me ship software. NOT WORK - a simple contact only based form(i.e. a fixed requirement having a name, email, textarea field and an upload file option), I rather test it manually than spend time writing tests
May i ask: what is your problem domain / field of work? I develop SAAS apps and complex HA web applications/API. I am web developer - PHP(Laravel to be exact).
We write extensive unit tests, but mostly after development work. The re-write work you mention is then avoided.
Which is often enough to ensure the code is testable.
Generally, I'll write some tests sort of alongside, or soon after (like, a couple hours or a day) to not lose the initial thought process. Going back to code days/weeks later and trying to 'test' it when it wasn't conceived of as testable is tough.
Indeed it does! But that doesn't really change whether you write the test pre- or post-coding.
To answer your question properly, you need to back up a bit. What is the benefit of TDD? If you answer is "To have a series of regression tests for my code", then I think the conclusion you will come to is that Test First is almost never the right way to go. The reason is that it's very, very hard to imagine the tests that you need to have for your black box code when you haven't already written it.
You might be wondering why on earth you would want to do TDD if not so that you can have a series of regression tests for your code. Remember that in XP there are two kinds of testing: "unit testing" and "acceptance testing". An acceptance test is a test that the code meets your requirements. In other words, it's a black box test regression test. You are very likely to do acceptance testing after the fact, because it is easier (caveat: if you are doing "outside-in", usually you will write an acceptance test to get you started, but after you have fleshed in your requirements, you normally go back and write more acceptance tests).
If acceptance tests are regression tests, why do we need unit tests. A common view of "unit tests" is to say that you want to test a "unit" in isolation. Often you take a class (or the equivalent) and test the interface making sure it works. Frequently you will fake/mock the collaborators. It makes sense that this is what you should do because of the words "unit" and "test".
However, originally this was not really the case as far as I can tell (I was around at the time, though not directly interacting with the principle XP guys -- mostly trying to replicate what they were doing in my own XP projects. This all to say that I feel confident about what I'm saying, but you shouldn't take it as gospel). Really right from the beginning there were a lot of people who disliked both the words "unit" and "test" because it didn't match what they were doing.
Let's start with "test". Instead of testing that functionality worked, what you were actually doing is running the code and documenting what it did -- without any regard for whether or not it fit the overall requirements. One of the reasons for this is that you don't want to start with all of the data that you need and then start to write code that produces that data. Instead you start with a very small piece of that data and write code that produces that data. Then you modify the data and update the code to match that data. It is less about "test first" as it is about decomposing the problem into small pieces and observing the results of your development. It does not matter if you write the test first or second, but it's convenient to write the test first because before you can write the code, you need to know what change you want the code to enact.
One of the reasons why the term "BDD" was invented was because many people (myself included) thought that the phrase "Test Driven Development" was misleading. We weren't writing tests. We were demonstrating behaviour of the code. The "tests" we were writing were not "testing" anything. They were simply expectations of the behaviour of the code. You can see this terminology in tools like RSpec. For people like me, it was incredibly disheartening that the Cucumber-like developers adopted the term BDD and used it to describe something completely different. Even more disheartening was that they were so successful in getting people to adopt that terminology ;-)
Getting back to the term "unit", it was never meant to refer to isolation of a piece of code. It was meant to simply describe the code you happened to be working with. If we wa...
I do for some projects. For example currently I'm working on a project that has a high test coverage and most bugs and enhancements start first as a test and then they're implemented in code. TDD makes sense when the test code is simpler to write than the implementation code.
> Can you describe the practical benefit?
It may take some time to write initial tests but as I'm working with some legacy enterprise tech serializing all inputs and testing on that is a lot faster than testing and re-testing everything every commit on real integration servers.
Tests provide you with a safety net when you do refactors or new features so that the existing stuff is not broken.
> Do you happen to rewrite the tests completely while doing the implementation?
Yeah, I do. There are two forces at play - one of them pushes to test that cover more stuff in a black-box matter - they won't be broken as often when you're switching code inside your black-box. On the other hand if you've got finer grained test when they break it's obvious which part of code is failing.
> When does this approach work for you and when did it fail you?
It works for projects that are hard to test other way (we've got QA but I want to give them stuff that's unlikely to have bugs) and for keeping regressions at bay. It did fail me if I didn't have necessary coverage (not all cases were tested and the bug was in the untested branch).
I wouldn't also bother to test (TDD) scratch work or stuff that's clearly not on critical path (helper tools, etc.) but for enterprise projects I tend to cover as much as possible (that involves sometimes writing elaborate test suites) as working on the same bugs over and over is just too much for my business.
I also believe that 100% test coverage (or numbers close to that) just isn't a useful goal, and is counterproductive from a maintenance perspective: test code is still code that has to be maintained in and of itself, and if it tests code that has a low risk of errors (or code where, if there are errors, those errors will bubble up to be caught by other kinds of testing), the ROI is too low for me.
After I've settled on interfaces and module boundaries, with a plausibly-working implementation, I'll start writing tests for the code with the highest risk of errors, and then work my way down as time permits. If I need to make large changes in code that doesn't yet have test coverage, and I'm worried about those changes causing regressions, I'll write some tests before making the changes.
Isn't that a separate issue from writing the tests before the implementation?
Often people fall back on manual testing, which is often slow, unreliable and incomplete. And certain things might not even be testable if the system hasn't been designed to allow it.
I guess I'm assuming we're talking about unit testing specifically here, so I'm considering functional and integration testing as separate items.
Manual testing is terrible. I'll occasionally engage in that, but only as an extremely-short-term measure when I'm (again) not convinced my interfaces are stable enough yet to write unit tests.
There are a couple of circumstances I often do, though.
The first is when fixing a bug - writing the (red) regression test first forces me to pin down the exact issue and adds confidence that my test works. Committing the red test and the fix to the test in two commits makes the bug and its fix easy to review.
The second is when I'm writing something high risk (particularly from a security standpoint). In this case I want to have a good idea of what I'm building before I start to make sure I've thought through all the cases, so there's less risk of rewriting all the tests later. There's also more benefit to having a thorough test suite, and I find doing that up front forces me to pin down all of the edge cases and think through all the implications before I get bogged down in the "how" too much.
I've done this in the past. Then I started to use `git bisect` and having a red test somewhere in your commit-history is a killer for bisect. So now I tend to include both, the test and the bug-fix, within one commit.
That way you can commit the test, bisect works, and the test begins "failing" when the bug is really fixed, and you can commit the fix as well as a one-line change to amend the test from being failure-expected to just a normal test.
Obviously if you view tests differently (eg. as a declaration of current behavior rather than intended behavior) then my argument dies.
@failing testDivZero() { assertEquals(None, div(1, 0)) }
This expresses both the intent and the reality
I don't think this is compelling argument. Normally the test and the fix are looked at together and are already sufficiently separated in the code.
It makes more sense to me to use a single commit so the fix can be described in a single commit message, keeping the history clean.
My workflow tend to be to keep them separate, and write a big description in the merge commit rather than the individual commits (those have a small description of the local change).
There's tradeoffs to both approach though, and as you mention, it's easier to keep track of the provenance of a fix in the git blame when it's unified in a single commit message.
Just write enough code to make the test pass, no more no less. Refactor and repeat.
In recent years I've never written any new piece of code without test first and can not be any happier. Beside the confidence a test gives you, it really a great way to pin down your thoughts and write what makes them meterialize.
BTW, I rarely work from a well-defined spec these days.
That being said, I never write all the possible tests before starting with the implementation. They're called unit tests for a reason -- I generally write at least a few tests for a particular unit (say, a function or method) and then write the implementation before starting working on another. And I often go back and add extra tests for an already implemented unit to cover some edge cases and error conditions.
Write the code, secure it from refactoring stuff ups with your tests.
Rewriting the tests completely does not really happen. Sometimes I am not entirely sure of all the things that the production code should do so then I go back an forth between test and executable code. In that case one needs to be very aware of whether a failure is a problem in the executable code or the test code.
It pretty much works all the time. Occasionally there are the exceptions. If a thing is only visual, e.g., in a webinterface it may be best to first write the production code because the browser in my head may not be good enough to write a correct test for it. Also, in the case of code that is more on the scientific/numeric side of things one may start out with more executable code per test than one usually would. I still write the test first in that case, though.
For new development, no.
I've found that unless I have a solid architecture already (such as in a mature product), I end up massively modifying, or even entirely rewriting most of my tests as development goes on, which is a waste of time. Or even worse, I end up avoiding modifications to the architecture because I dread the amount of test rewrites I'll have to do.
I like BDD, it helps me focus on the goal.
I feel that it lets me find the right approach faster.
Also, helps avoid distractions and optimizing things too early.
Related: „Write tests. Not too many. Mostly integration.”, httsps://kentcdodds.com/blog/write-tests/
I could write a test plan first, but I haven't always fully designed the interfaces until I've ploughed into the code and figured out what needs to be passed where, so there would be a lot of repeat effort in fixing up the tests afterwards.
Effectively, in writing tests first you make assumptions about the code. These don't always turn out to be true.
The tests never caught a bug for the first 3 years. Got in the way a lot though.
Then they finally caught 1.
Not worth it.
And even with TDD, I don't often find myself breaking a lot of things that were already working, though it does happen. In those infrequent cases, it's extremely valuable to know I broke stuff that was working. I.e., it's pretty sad to ship changes that broke other behaviors, things you had the faintest clue that you were impacting.
What I do see gobs of, when doing TDD, is the tests preventing crap code from getting integrated in the first place, i.e. when I or others first write the code (or change the code of others). From the testing perspective, that's the real thing they do--gate the defects from ever leaving your desktop, and in a far faster manner than most other routes.
Unless, of course, one is a perfect coder.
In any case, TDD has more important benefits that I've also gotten. Easily worth it for me.
I do TDD on Core, especially on mission critical code.
The Shell however have almost zero automated tests.
There are also some collection of links in github such as https://gist.github.com/kbilsted/abdc017858cad68c3e7926b0364....
The pattern articulated by Gary also best resonated my thoughts and experience in building software.
Other situations are coming just from experience: you know that some part will have a lot of special cases, so you implement a test as soon as you think of the next special case.