You likely often hear that test-driven development (TDD) or just writing tests can make your code better. It’s hard to say whether this is true unless you have seen the impact of writing unit tests on code before. Let’s take a look at this effect with a simple example: moving an internal state of a class to a dependency.
The most straightforward way of keeping a state is to add a private variable and put aside the values you need for later use. This will do the job, but it makes testing more difficult. For complete test coverage, you would need to:
As you add more internal variables, achieving the expected state becomes exponentially more complicated: besides simple states, you will need to include combinations between them. There can be invalid combinations that are impossible to achieve if everything works as expected—but you could be interested in testing whether the code gracefully degrades if it reaches this impossible condition.
There is also a temptation to access the class’s private variables from the test, but this approach feels wrong. It depends on knowing the implementation of the class, and it ignores the interface we are defining for the type.
To make the code more testable, we can define a separate class that will keep the state. This new class introduces a layer with a new interface surface that we can use to describe a relationship between objects. You can mock methods used to set and retrieve the state changes, making testing easier.
As an example with low testability, we will have an alarm clock class:
We can expect certain behaviors from this clock:
If you wanted to test this behavior, you would need one of two approaches:
Approach 1 is wrong; it could require hours of waiting.
Approach 2 has downsides, too, they’re just more subtle. It would require your tests to read the current time and add seconds of waiting for results. Setting the wait time would be a trade-off between stable tests, where we wait long enough to avoid missing the point on slow machines, and waiting too long and slowing down the testing.
We can make this code more testable by moving the state outside:
So, in this case, introduce a dependency— AlarmTimeStore—which keeps the value set outside the AlarmClock class.
As we moved the state outside, we introduced a dependency to facilitate testing. When we run the class in the test, we replace the actual dependencies with mocks. Mocks are a drop-in replacement for other instances that provides the same interface but allows for setting expectations. You can provide a value that that function calls will return.
Mocking allows you to run the class or function in isolation from the other code—you can thus control what values are returned to your unit and check whether it’s behaving as expected.
By moving the state outside, we define a clear separation between those two classes. As the persistence layer gets a clearly defined interface, it will be easier in the future to:
Some people criticize this approach for introducing too many layers of abstraction to implement features. I got feedback like this for the code I wrote when trying to keep it very testable. Most likely, it’s a matter of personal taste and beliefs about unit tests: I like my code to be covered by tests, and I’m happy with subtle changes to the code design to make sure it’s easily testable. If somebody doesn’t care about tests, I’m not sure if there are strong arguments to make that this approach is objectively better than the alternatives. The value of the flexibilities I’ve listed above depends on how likely we are to actually need them at some point. If it’s probable that you won’t need them, you could argue that it’s a premature act of preparation for a use case we might never need.
Are you writing tests regularly? Please share how it impacts your code—I would love to hear stories from you guys!
Also Published Here