Writing small, focused tests, often called unit tests, is one of the things that look easy at the outset but turn out to be more delicate than anticipated. Writing a three-lines-of-code unit test in the triple-A structure soon became second nature to me, but there were lots of cases that resisted easy testing.
Using mock objects is the typical next step to accommodate this resistance and make the test code more complex. This leads to 5 to 10 lines of test code for easy mock-based tests and up to thirty or even fifty lines of test code where a lot of moving parts are mocked and chained together to test one single method.
So, the first reaction for a more complicated testing scenario is to make the test more complicated.
But even with the powerful combination of mock objects and dependency injection, there are situations where writing suitable tests seems impossible. In the past, I regarded these code blocks as “untestable” and omitted the tests because their economic viability seemed debatable.
I wrote small tests for easy code, long tests for complicated code and no tests for defiant code. The problem always seemed to be the tests that just didn’t cut it.
Until I could recognize my approach in a new light: I was encumbering the messenger. If the message was too harsh, I would outright shoot him.
The tests tried to tell me something about my production code. But I always saw the problem with them, not the code.
Today, I can see that the tests I never wrote because the “test story” at hand was too complicated for my abilities were already telling me something important.
The test you decide not to write because it’s too much of a hassle tells you that your code structure needs improvement. They already deliver their message to you, even before they exist.
With this insight, I can oftentimes fix the problem where it is caused: In the production code. The test coverage increases and the tests become simpler.
Let’s look at a small example that tries to show the line of thinking without being too extensive:
We developed a class in Java that represents a counter that gets triggered and imposes a wait period on every tenth trigger impulse:
public class CountAndWait {
private int triggered;
public CountAndWait() {
this.triggered = 0;
}
public void trigger() {
this.triggered++;
if (this.triggered == 10) {
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
this.triggered = 0;
}
}
}
There is a lot going on in the code for such a simple functionality. Especially the try-catch block catches my eye and makes me worried when thinking about tests. Why is it even there? Well, here is a starter link for an explanation.
But even without advanced threading issues, the normal functionality of our code is worrisome enough. How many lines of code will a test contain that covers the sleep? Should I really use a loop in my test code? Will the test really have a runtime of one second? That’s the same amount of time several hundred other unit tests require for much more coverage. Is this an economically sound testing approach?
The test doesn’t even exist and already sends a message: Your production code should be structured differently. If you focus on the “test story”, perhaps a better structure emerges?
The “story of the test” is the description of the production code path that is covered and asserted by the test. In our example, I want the story to be:
“When a counter object is triggered for the tenth time, it should impose a wait. Afterwards, the cycle should repeat.”
Nothing in the story of this test talks about interruption or exceptions, so if this code gets in the way, I should restructure it to eliminate it from my story. The new production code might look like this:
public class CountAndWait {
private final Runnable waiting;
private int triggered;
public static CountAndWait forOneSecond() {
return new CountAndWait(() -> {
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
public CountAndWait(Runnable waiting) {
this.waiting = waiting;
this.triggered = 0;
}
public void trigger() {
this.triggered++;
if (this.triggered == 10) {
this.waiting.run();
this.triggered = 0;
}
}
}
That’s a lot more code than before, but we can concentrate on the latter half. We can now inject a mock object that attests to how often it was run. This mock object doesn’t need to sleep for any amount of time, so the unit test is fast again.
Instead of making the test more complex, we introduced additional structure (and complexity) into the production code. The resulting unit test is rather easy to write:
class CountAndWaitTest {
@Test
@DisplayName("Waits after 10 triggers and resets")
void wait_after_10_triggers_and_reset() {
Runnable simulatedWait = mock(Runnable.class);
CountAndWait target = new CountAndWait(simulatedWait);
// no wait for the first 9 triggers
Repeat.times(9).call(target::trigger);
verifyNoInteractions(simulatedWait);
// wait at the 10th trigger
target.trigger();
verify(simulatedWait, times(1)).run();
// reset happened, no wait for another 9 triggers
Repeat.times(9).call(target::trigger);
verify(simulatedWait, times(1)).run();
}
}
It’s still different from a simple 3-liner test, but the “and” in the test story hints at a more complex story than “get y for x”, so that might be ok. We could probably simplify the test even more if we got access to the internal trigger count and verify the reset directly.
I hope the example was clear enough. For me, the revelation that test problems more often than not have their root cause in production code is a clear message to improve my ability on writing code that facilitates testing instead of obstructing it.
I don’t shoot/omit my messengers anymore even if their message means more work for me.
I think you can improve the code-under-test even further by employing “return first” (https://schneide.blog/2021/01/18/return-first/). In this case, you can return from trigger() before calling into this.waiting by returning an enum or a bool for “DoNothing” and “Wait”.