Writing The Seemingly Trivial Test

Many people have asked how much time and effort they should spend testing the seemingly “trivial” code that they write. When I first started writing tests, I myself wondered about what types of tests were appropriate to write and how much benefit I would get from testing what seemed to be very trivial code. If the code I am writing is so simple that it can’t possibly be wrong, why should I spend time writing tests for it? What value do those tests provide? A classic example is verifying that preconditions are enforced during object instantiation. It may seem reasonable to test a precondition such as:

    @Test(expected= IllegalArgumentException.class)
    public void theOriginAndDestinationCanNotBeTheSame(){
        Location home = ...;
        new Route(home, home);
    }

This is a precondition for instantiating a new Route as well as being an important requirement for a Route in general.  It seems clear that this is a good requirement to write a test for.   But in my experience, the vast majority of preconditions in a system are more basic and fundamental.   They could include assuring a collection is not empty, or that a String is not empty, or the most prevalent of them all, that an object is not null.

    @Test(expected = IllegalArgumentException.class)
    public void ifYouAttemptToCreateAnEncrytorWithANullKeyAnIllegalArgumentExceptionIsThrown() {
        new BasicStringEncryptor(null);
    }

Should you write tests for all of these seemingly trivial scenarios, especially the null checks? I’ve come to the conclusion that no test is too trivial if it captures an important feature or requirement of the code under test. This is especially true if the code under test is a public API. Here is my reasoning behind this:

  • Catch the typo, the copy and paste error, or the accidentally selected the wrong auto code completion option: This is probably the least important reason but I’ve definitely seen these problems more than a few times in what you would think is trivial code.
  • Writing the test forces you to think about how the consumer of the code will try to comprehend and attempt to us the code.
  • What you think is simple and obvious isn’t always so: What you view as obvious while writing the code may not be so obvious to the many people who will read your code as they attempt to enhance or maintain it.
  • The tests provide a live, runnable specification: The specification can document the expected usage of the code as well as what you can and can’t do with the code. For example:
  •     @Test(expected = IllegalArgumentException.class)
        public void ifYouAttemptToCreateAnEncyprotWithANullKeyAnIllegalArgumentExceptionIsThrown() {
            new BasicStringEncryptor(null);
        }
     
        @Test
        public void anEncryptorCanBeCreatedWithAValidKeyAndANullProviderClass() {
            assertThat(new BasicStringEncryptor(new Key(), null), is(notNullValue()));
        }
  • The tests provide a hint to the future developer that they need to think twice about changing this behavior: Countless times I have wondered whether or not I could safely change seemly trivial bits of code not knowing whether or not the behavior was assumed to be part of the specification or if the original developer had even considered it.
  • If it is a public API, people may be depending on that behavior: This goes hand and hand with the pervious point.

The examples to this point have been weighted towards simple preconditions, which I think is only one type of seemingly trivial test. Another scenario is a test whose implementation is very similar to the implementation of the code under test. Consider the following test:

    @Test
    public void theLocationIsTheIPAddressOfTheNodeWhereTheCodeIsRunning()
            throws UnknownHostException {
        InetAddress localhost = InetAddress.getLocalHost();
        Event event = ...
        assertThat(event.getLocation(), is(equalTo(localhost.getHostAddress())));
    }

The implementation behind getLocation() is probably pretty similar to the code in the test. So why would you want to write virtually the same code in the test?

  • Again, the test provides a live specification
  • It allows you to make sure your code really does work at a micro-level before you incorporate it into a higher level acceptance test
  • The code may be refactored as some point: In the example above, you may have realized that your original implementation wasn’t performing as well as it needs to because each call to InetAddress.getLocatHost() required a DNS lookup. So you decide to cache the lookup or use a static field to store the result. The implementation is suddenly less trivial and the test case exists to verify your changes.

I recently heard an interview with “Uncle Bob” Martin where he compared a developers use of TDD and writing tests to an accountant’s use of double-entry bookkeeping. Double-entry bookkeeping is a pretty good metaphor for the types of tests I describe here (and for tests in general, which is basically Uncle Bob’s point).

So what about the time involved in writing these tests. I don’t buy the argument that writing these types of tests takes too much time. Many of these test will take you no more than 30 seconds to write, are very easy to read, and run very quickly. So give it a try and let me know what you think.


1,664 Responses to “Writing The Seemingly Trivial Test”

Leave a Reply