Skip to main content

How unit testing changes your design

Lots of developers feel that unit tests are not only hard to write, but also are more of a hinder than a help when working with the code.

I think this comes largely from having code which is not “suitable” for unit testing, or put differently, that unit testing is not suitable for that code base. To get the promised value out of unit tests I think it helps to look at how unit testing effects the implementation code.

In my experience, whether some code is suitable for unit testing or not depends largely on two factors.

  • Does it have enough complexity so it is worth writing unit tests for?
  • Does it have few dependencies so that isolating it under test is simple?

While there are other factors that affect testability, I’ve found these to be the most important.

Enough complexity #

The first major factor for testability deals with “is this code complex enough to need a test?” Not every line of code, every method, or even every class needs a separate unit test! This is, quite eloquently, summarized by Kent Beck, father of Test-Driven Development and JUnit.

I try not to have more tests than I need. – Kent Beck

The typical example is a “getter”, a method which does nothing but returning a value of a variable. Does it need a separate test? I would say “no”. There are two primary reasons for this, in my mind.

  1. It is too simple to reasonably fail. In any case, the extremely occasional failure because of an invalid getter will most likely cost us much less than writing and maintaining unit tests for all getters.
  2. Other tests will cover that code. A getter rarely exists for itself. It is merely a method for some object to get information from another one. That information is then used for some computation or other purpose. The tests for that other code will test the getter “for free”.

Somewhat tongue-in-cheek, unit testing is like a grilling meat. There is no point in putting a bone without meat on the grill. (On the flip side, if you put too much meat on a weak grill, it won’t get properly done.)

If you develop using Test-Driven Development, you get this for free. Unless the getter is a major feature of your system, you probably won’t write code to tests that getter. You won’t do it because having that getter does not “drive” the design of the system, it is just an implementation detail.

Finally, it is not okay to keep a test for something that can’t reasonably break “just because”. It still takes time to maintain. If it doesn’t provide any real value, remove it!

Few dependencies #

The second major factor for testability is about having few dependencies. It is so obvious it is often missed – the fewer dependencies, the easier to test.

As I said, it is really obvious. After all, the purpose of unit testing is testing a single unit. While doing so, we try to isolate that unit as much as possible. We want to test that unit, not the other next to it. This has lead to a big rise in the use of mocking frameworks such as Mockito and EasyMock.

But as I also said, it is so obvious that the real point is often missed. While we can fake dependencies during testing with frameworks, the best way to make isolating a unit easier is to reduce the number of dependencies it actually has! It doesn’t take a Mensa membership to figure that out, but for some reason (us being engineers, not philosophers) we forge ahead and try to solve the problem with clever tools without realizing that we are solving the wrong problem.

Interestingly enough, fewer dependencies also means easier to reuse. Code with fewer dependencies is less tightly coupled to the specific application code, and instead deals with more general concepts in the model.

Implications on design #

These two principles actually has some fundamental implications on the design of the code.1

The classes in two code bases in which unit testing will be easy and hard, respectively, plotted on a graph with axes for “Number of dependencies” and “Code complexity” would look like this.

Two possible code bases that are more or less easy to test.

As for the design and testing of a system, we see two clear trends.

  • Most complexity is in classes with few dependencies. Unit test this code as much as you want. Writing tests is easy because each unit is well isolated.
  • Most dependencies are in classes with low complexity. Test setup is hard for these classes. Consider not unit test these classes at all, instead focusing on automated integration tests.

The separation between complexity and dependencies aligns very well with the functional core, imperative shell pattern that is part of Functional foundations.

Now I know that much (perhaps most) code doesn’t look this way. There is plenty of code which mixes complexity and dependencies. I believe this is unfortunate as it complicates unit testing and generally is harder to understand and work with. I think the two statements above are worth striving for. That will help a team to get true value out of their unit tests.

Updates #

  • 2024-04-24: Republished this post which was originally written for my previous blog. Added a paragraph describing the link to “functional core, imperative shell”.

  1. My post Testable code is reusable code looks at the relation between testability and good design from another perspective. ↩︎