4 ms·
I'll be frank. I hate arguments like this: reductio ad absurdum. However, let's try and save this blog post. Why, exactly, have we run into a problem trying
by mikekchar 8y ago
I'll be frank. I hate arguments like this: reductio ad absurdum. However, let's try and save this blog post. Why, exactly, have we run into a problem trying to test drive "Hello, world"?
First, let's ask the question: what is the important output of "Hello, world"? What are we trying to accomplish. Usually it is a program to see if the compiler is working. It is, in itself, an acceptance test. We don't TDD acceptance tests generally.
However, let's say that we want to write a program that outputs a string literal to STDOUT for some reason. Is the string literal itself an important part of the functioning of the program? If I change the literal, will the program function badly? This actually gets to the heart of "unit test" versus "acceptance test". With an acceptance test we want to write tests that match our implementation with business requirements. However, with unit tests our goals are much more humble. We wish to alert ourselves when modification to the code has changed the behaviour of the program. We do not actually need to assert that the behaviour matches the business requirements (that's what acceptance tests are for!)
This solves one of the dilemmas in the original article: No, there is no need to make "Hello, world" more general. And we do not actually need to test that it outputs "Hello, world". Instead, we need to have an acceptance test (which might be a manual test) that shows that it outputs "Hello, world" and we need unit tests to show that changes to our implementation do not change behaviour.
In other words, for a unit test, we should concentrate on whether or not we have written something to the stream. We may even want to check to see if that something is the string literal (which we could put in a constant for easy access). However, there is no need to assert that the string literal is, in fact, "Hello, World". I really can't stress enough the importance of this distinction in writing good unit tests.
So I need a library that has a string literal as a constant and a function that takes a stream and prints the string literal to it. Then I need a test that sets up the stream, runs the function and reads the contents of the stream. This is not any more complicated than the problem deserves.
Do we need to test that main, run the code? I think this is a very interesting question. Again, we need to go back to the difference in purpose between an acceptance test and a unit test. If we can keep the code in main down to a single line, then perhaps we can simply have an acceptance test (possibly manual) that tests that the program starts up properly and unit tests for the behaviour of the program. If main starts to have more than one behaviour (startup), then we will have to refactor it.
What happens when we do want the program to print something other than a string literal? That's when we start adding complexity to the program.
If you find that you are adding unnecessary complexity to your program in order to write unit tests, usually it's a good sign that you are approaching your unit testing in a less than optimal fashion. There are cases where it is extremely tricky to write good unit tests (my favourite example is trying to test a function that outputs an infinite series -- like the other favourite "TDD doesn't work" example, Fizz Buzz).
I hope some people find this interesting/useful. Like I said, I generally hate arguments like the one the article presents and I usually try not to respond to them, but I was at a loose end tonight.
Edit: Nonsensical wording.