Ask HN: Do you write tests before the implementation?

248 points by MichaelMoser123 ↗ HN
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 ] thread
Almost never. I’m roughing things out first, or iterating the APIs. When the functions, data and interactions seem to stabilize, then I’ll start to put tests in.

Once, 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.

I have a similar experience, but now I am forced to use Ginkgo, and the tool doesn't make sense without TDD and BDD - behavior driven development, so some people must be using it.
Hi, I sort of maintain Ginkgo and Gomega when I have time (not much these days), having picked it up during my years at Pivotal, where it was originally developed. BDD/TDD is practiced extensively (as in, 100% of the time) at Pivotal. I'd be happy to talk to you more about the process or tools if you would like. Good luck!
I recently started doing this. My project involved using three different services, where one of them was internal. I only had API documentation for these services and because of many reasons, there was a delay in obtaining the API keys required and I was stuck on testing my code. That's when I decided to write unit tests and mock these services wherever I am using and started testing my code. There were zero bugs in these integrations later.

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.

Thanks, an interesting perspective. Do you plan to go with this approach for your other projects as well?
I am planning to do this for my hobby projects. I think it will help me write better code and also learn a few things. For professional projects, I think the time I will try to add stuff but it relies on my manager's approval.
More fun is when you get an API documentation and no access to the actual system. You develop the whole thing and then fly out to their site, you've got 3 days to get your software and hardware certified by them, and the certification costs a fortune.
I mean how many of you stick with this test driven development practice consistently? I have been doing this for a while now. Practically, saves me a tonne of time and am able to ship software confidently.

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

Thanks for your perspective. Did it take a lot of time to develop the required discipline? I mean defining the interface for a single function is different from defining a set of functions in the context of a test. May i ask: what is your problem domain / field of work?
Did it take a lot of time to develop the required discipline? I wouldn't say lot of time. If you get a good mentor, it isn't a steep learning curve. Moreover, I program using a framework, it has lot of helper functions.

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).

cannot agree more. i've worked for a year on a fast evolving software and we had to refactor things a lot. TDD helped me to refactor in confidence and without regressions. Now i can't live without tests!
The benefits you describe seems to be achievable with tests written after code as well.

We write extensive unit tests, but mostly after development work. The re-write work you mention is then avoided.

The benefit of TDD is that the code you end up with will actually be testable. Just keeping in mind that you have to write a test for your code, changes how you write it. As a bad example, imagine having a 1000 line function that just does everything you needed for the new feature... Good luck testing that afterwards.
> Just keeping in mind that you have to write a test for your code, changes how you write it.

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.

> Just keeping in mind that you have to write a test for your code, changes how you write it.

Indeed it does! But that doesn't really change whether you write the test pre- or post-coding.

Test Driven Development is not actually synonymous with Test First Development. Test First is a method that you can use to do TDD. It's quite a good method, but it's not the only one.

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...

Thanks for your perspective. One issue is that it is often hard to tell in advance whether you know what you are doing or not. Also in my experience some implementation details can lead to a revision of the interface as well.
My best advice is to try it both ways (assuming you are interested in Test First/TDD). You'll find a sweet spot that works well for you. This is an area where I think there are lots of things that can work well. For me, my TDD is probably the sharpest knife in my kit, so I rely on it. For others, maybe there are other things. Don't let anyone tell you that there is only one way to do it. Of course you have to find a way to collaborate effectively (and that's the real difficult part), but in terms of growing as a developer I think you've got a lot of viable paths.
Thanks for your advise. I think it is an advantage to learn about different ways to look at a problem. Thankfully there are a lot of ways to look at problems when working in the software business.
> I mean how many of you stick with this test driven development practice consistently?

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.

Never done this, and don't consider it practical. Code and interfaces (even internal ones) change rapidly for me when I'm starting a new project or adding new major functionality to the point that the tests I'd write at the beginning would become useless pretty quickly.

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.

That is how I used to work; then I got into finance and there are two things different with the work I did before that (web/desktop/app (or too long ago; there was no 'testing' in the 80s) the software I write now has to be certified/audited to some extent and I cannot change/repair production software on the fly. That could costs a lot of money for certain bugs. So now I tend to write tests for everything and that helps a lot.
>So now I tend to write tests for everything and that helps a lot.

Isn't that a separate issue from writing the tests before the implementation?

Agreed, and the kind of software you’re working on is a part of this conversation that often gets left out and leads to people talking last each other. I work in cryptography, and our work needs an exceptionally high level of testing to catch subtle bugs. I used to work in games and I didn’t see the same value out of tests. Different industries require different practices.
If I worked on software like that, I would almost certainly write more tests. But still not up-front.
What "other kinds of testing" do you do instead then? How do you make sure the code is testable by those other tests?

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.

Functional and integration testing, mostly.

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.

I've been asked to literally test that values are incremented. Like, does ++ add 1 to the value in C, just to be safe, for the sake of 100% coverage.
I've seen tests that essentially look like that, and tests that, when you really look at it, pretty much just test the testing framework and mocking library and don't really exercise application code. It's infuriatingly useless.
The majority of the time, no.

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.

> Committing the red test and the fix to the test in two commits makes the bug and its fix easy to review.

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.

A tip I learned is to commit the failing test but mark it as an expected failure, if your test framework supports that.

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.

this is why i love HN. that is such an outstanding idea.
I see a test as a declaration of intended outcome. By writing a test to expect an intentional failure (say you have a bug in a divide: “int -> int -> Maybe int” function that causes it to return 0 when you divide by 0 instead of “None”) you are declaring that is actually intentional behavior. So I would never write a test like this - I think I would prefer committing the fix and the new test at once. I don’t see the value in reviewing them separately, because they are related and dependent changes.

Obviously if you view tests differently (eg. as a declaration of current behavior rather than intended behavior) then my argument dies.

Keep in mind the test is written with the correct behaviour and annotated to be failing — in a hypothetical language and framework your test would be

@failing testDivZero() { assertEquals(None, div(1, 0)) }

This expresses both the intent and the reality

I get around this by squashing the PR commits. Reviewers see the individual commits, and good CI means they can see the test-commit failed and the fix-commit passed, but post-merge it's a single passing commit.
> Committing the red test and the fix to the test in two commits makes the bug and its fix easy to review.

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.

Keeping it separate allows ensuring the test works easily: just revert the fix keeping only the patch for the test and make sure the test fails.

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.

I totally understand why you would do that and it makes sense, but I personally like to keep them in one commit so that I won't get any problems with an automated git bisect later.
No. There are situations where I do that, when I am adding new option to existing library where API is already set and clear.
Almost never when writing new code. It feels like editing a manuscript before it even exists. If I "write" the code in pseudocode in my head or on paper first, I might write some tests first.
tests can be used to clarify your intentions and what you are attempting to build. It's best place when you are actually starting from a clean slate.

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.

Yeah... I have read that and told that to others many times. But the thing is some of my best work (rare, though) has not been to spec. To any spec. More like, when you are doodling on a paper. "This might be a lake with a duck. Nope scratch that, it's actually a dragon and those are its scales. Yep."
Yes, I understand what you mean. When I face uncertainty, I explore with not so much emphasis on testing. But once I am over with that exploratory phase, I funnel the learning into the TDD process to solidify my implementation and guide my design.

BTW, I rarely work from a well-defined spec these days.

Definitely, whenever I have a clearly defined design to implement. If I don't, I generally try to design the API/interface first, then write at least the basic tests before implementing.

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.

Nope. Can't stand TDD.

Write the code, secure it from refactoring stuff ups with your tests.

Yes, I am pretty consistent in it. It has the great benefit of leading to highly reliable software even in the face of complex requirements and many requests for changes.

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 maintenance & extension, yes.

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.

What period do you consider swapping the definition from new development to extension?
For new development, no. for the last 10 years I realise that stuff will change fast. when is solid, and feature is clear and will remain same for at least few months, yes, then I do it :)
This is a great hoax in software industry. I generally use it for conversation starter in company's lunch
Yes.

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/

Personally, nope.

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.

No, because I'm always under pressure to show something on a screen as early as possible, but I really wish I did.
No, although sometimes I do wish I were working on problems that are so simple to test.
No, in fact, we don’t use coded tests at all, deliverables are tested by the product owner(s). On the code level, we hardly see bugs, not even when refactoring. I often wonder if it is luck or expertise, and whether we would benefit from writing tests.
Maybe you hardly see bugs because you don't test for them.
I used to wonder the same thing, until I inherited a project with tests.

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.

I don't see unit tests "catch bugs" often, either, in the sense that the CI build fails due to defective code pushed up.

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.

If you tested, you wouldn’t have to wonder.
That’s Schrödinger's unit test, I guess.
I follow the Functional Core, Imperative Shell pattern.

I do TDD on Core, especially on mission critical code.

The Shell however have almost zero automated tests.

Thanks for that term, it sounds related to the style I prefer. Do you have any recommended references?
Only when fixing a bug (job done when tests pass) or reworking something that I believe isn't well tested (job done when I'm happy with the refactor & tests still pass).
When I contemplate how I'm going to implement it and it starts to look overwhelming and tedious then I go and write some tests.

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.

Only when I'm sure the requirements won't change while I'm writing the implementation. Which is 5%-10% of the time at best.