In the Real World, it's perfectly normal to write unit tests for someone else's code. Sure, the original developer should have done this already, but often you receive legacy code where this just wasn't done. By the way, it doesn't matter whether that legacy code came decades ago from a galaxy far, far away, or whether one of your coworkers checked it in last week, or whether you wrote it today, legacy code is code without tests
Ask yourself: why do we write unit tests? Going Green is obviously just a means to an end, the ultimate goal is to prove or disprove assertions about the code under test.
Say you have a method that calculates the square root of a floating-point number. In Java, the interface would define it as:
public double squareRoot(double number);
It doesn't matter whether you wrote the implementation or whether someone else did, you want to assert a few properties of squareRoot:
- that it can return simple roots like sqrt(4.0)
- that it can find a real root like sqrt(2.0) to a reasonable precision
- that it finds that sqrt(0.0) is 0.0
- that it throws an IllegalArgumentException when fed a negative number, i.e. on sqrt(-1.0)
So you start writing these as individual tests:
@Test
public void canFindSimpleRoot() {
assertEquals(2, squareRoot(4), epsilon);
}
Oops, this test already fails:
java.lang.AssertionError: Use assertEquals(expected, actual, delta) to compare floating-point numbers
You forgot about floating point arithmetic. OK, you introduce double epsilon=0.01
and go:
@Test
public void canFindSimpleRootToEpsilonPrecision() {
assertEquals(2, squareRoot(4), epsilon);
}
and add the other tests: finally
@Test
@ExpectedException(IllegalArgumentException.class)
public void throwsExceptionOnNegativeInput() {
assertEquals(-1, squareRoot(-1), epsilon);
}
and oops, again:
java.lang.AssertionError: expected:<-1.0> but was:<NaN>
You should have tested:
@Test
public void returnsNaNOnNegativeInput() {
assertEquals(Double.NaN, squareRoot(-1), epsilon);
}
What have we done here? We started out with a few assumptions about how the method should behave, and found that not all were true. We then made the test suite Green, to write down proof that the method behaves according to our corrected assumptions. Now clients of this code can rely on this behavior. If someone were to exchange the actual implementation of squareRoot with something else, something that for example really threw an exception instead of returning NaN, our tests would catch this immediately.
This example is trivial, but often you inherit large pieces of code where it's unclear what it actually does. In that case, it is normal to lay a test harness around the code. Start with a few basic assumptions about how the code should behave, write unit tests for them, test. If Green, good, write more tests. If Red, well now you have a failed assertion that you can hold against a spec. Maybe there's a bug in the legacy code. Maybe the spec is unclear about this particular input. Maybe you don't have a spec. In that case, rewrite the test such that it documents the unexpected behaviour:
@Test
public void throwsNoExceptionOnNegativeInput() {
assertNotNull(squareRoot(-1)); // Shouldn't this fail?
}
Over time, you end up with a test harness that documents how the code actually behaves, and becomes sort of a coded spec. If you ever want to change the legacy code, or replace it with something else, you have the test harness to verify that the new code behaves the same, or that the new code behaves differently in expected and controlled ways (for example that it actually fixes the bug you expect it to fix). This harness doesn't have to be complete on day one, in fact, having an incomplete harness is almost always better than having no harness at all. Having a harness means you can write your client code with more ease, you know where to expect things to break when you change something, and where they broke when they eventually did.
You should try to get out of the mindset that you have to write unit tests just because you have to, like you would fill out mandatory fields on a form. And you should not write unit tests just to make the red line green. Unit tests are not your enemies, unit tests are your friends.