Test-Driven Development: Three Easy Mistakes
Join the DZone community and get the full member experience.
Join For FreeI run introductory training in test-driven development quite frequently these days. And each time I do, I find the same basic mistakes cropping up every time, even among teams who already claim to practice TDD. Here are the three mistakes I see most often:
1. Starting with edge cases (empty string, null parameter) or error cases:
Imagine you have to test-drive the design of an object that can count the number of occurrences of every word in a string. I will often see some or all of these tests written before any others:
public class WordCounterTest { @Test public void wordCounterCanBeCreated() { assertNotNull(new WordCounter()); } @Test(expected=IllegalArgumentException.class) public void nullInputCausesExeption() { new WordCounter().count(null); } @Test public void emptyInputGivesEmptyOutput() { Map<String, Integer> actual = new WordCounter().count(""); assertEquals(new HashMap<String, Integer>(), actual); } }
These feel easy to write, and give a definite feeling of progress. But that is all they give: a feeling of progress. These tests really only prove to ourselves that we can write a test.
When written first like this, they don’t deliver any business value, nor do they get us closer to validating the Product Owner’s assumptions. When we finally get around to showing him our work and asking “Is this what you wanted?”, these tests turn out to be waste if he says “Actually, now I see it, I think I want something different”.
And if the Product Owner decides to continue, that is the time for us to advise him that we have some edge cases to consider. Very often it will turn out to be much easier to cope with those edge cases now, after the happy path is done. Some of them may now already be dealt with “for free”, as it were, simply by the natural shape of the algorithm we test drove. Others may be easy to implement by adding a Decorator or modifying the code. Conversely, if we had started with the edge cases, chances are we had to work around them while we built the actual business value — and that will have slowed us down even more.
So start with tests that represent business value:
@Test public void singleWordIsCounted() { Map<String, Integer> expected = new HashMap<String, Integer>(); expected.put("happy", 2); assertEquals(expected, new WordCounter().count("happy happy")); }
This way you will get to ask the Product Owner that vital question sooner, and he will invest less before he knows whether he wants to proceed. And you will have a simpler job to do, both while developing the happy path, and afterwards when you come to add the edge cases.
2. Writing tests for invented requirements:
You may think that your solution will decompose into certain pieces that do certain things, and so you begin by testing one of those and building upwards from there.
For example, in the case of the word counter we may reason along the following lines: “We know we’ll need to split the string into words, so let’s write a test to prove we can do that, before we continue to solve the more difficult problem”. And so we write this as our first test:
@Test public void countWords() { assertEquals(2, new WordCounter().countWords("happy monday")); }
No-one asked us to write a method that counts the words, so yet again we’re wasting the Product Owner’s time. Equally bad, we’ve invented a new requirement on our object’s API, and locked it in place with a regression test. If this test breaks some time in the future, how will someone looking at this code in a few months’ time cope with that: A test is failing, but how does he know that it’s only a scaffolding test, and should have been deleted long ago?
So start at the outside, by writing tests for things that your client or user actually asked for.
3. Writing a dozen lines of code in order to get the next test to pass:
When the bar is red and the path to green is long, TDD beginners often soldier on, writing an entire algorithm just to get one test to pass. This is highly risky, and also highly stressful. It is also not TDD.
Suppose you have these tests:
@Test public void singleWordIsCounted() { assertEquals("happy=1", new WordCounter().counts("happy")); } @Test public void repeatedWordIsCounted() { assertEquals("happy=2", new WordCounter().counts("happy happy"counts)); }
And suppose you have been writing the simplest possible thing that works, so your code looks like this:
public class WordCounter { public String counts(String text) { if (text.contains(" ")) return "happy=2"; return "happy=1"; } }
Now imagine you picked this as the next test:
@Test public void differentWords() { assertEquals("happy=1 monday=1", new WordCounter().counts("happy monday")); }
This is a huge leap from the current algorithm, as any attempt to code it up will demonstrate. Why? Well, the code duplicates the tests at this point (“happy” occurs as a fixed string in several places), so we probably forgot the REFACTOR step! It is time to remove the duplication before proceeding; if you can’t see it, try writing a new test that is “closer” to the current code:
@Test public void differentSingleWordIsCounted() { assertEquals("monday=1", new WordCounter().counts("monday")); }
We can now make this simpler set of tests pass easily, effectively by removing the duplication between the code and the tests:
public class WordCounter { public String counts(String text) { String[] words = text.split(" "); return words[0] + "=" + words.length; } }
After making this relatively simple change, we have now test-driven part of the algorithm with which we struggled earlier. At this point we can try the previous test again; and this time if it is still too hard, we may wish to ask whether our chosen result format is helping or hindering…
So if you notice that you need to write or change more than 3-4 lines of code in order to get to green, STOP! Revert back to green. Now either refactor your code in the light of what just happened, so as to make that test easier to pass, or pick a test closer to your current behaviour and use the new test to force you to do that refactoring.
The step from red bar to green bar should be fast. If it isn’t, you’re writing code that is unlikely to be 100% tested, and which is prone to errors. Choose tests so that the steps are small, and make sure to refactor ALL of the duplication away before writing the next test, so that you don’t have to code around it whilst at the same time trying to get to green.
Published at DZone with permission of Kevin Rutherford, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Trending
-
How AI Will Change Agile Project Management
-
Application Architecture Design Principles
-
What Is JHipster?
-
10 Traits That Separate the Best Devs From the Crowd
Comments