Unit Testing – Does Immutability Reduce the Scope of Unit Testing?

immutabilityunit testing

Recently, I worked on an Android project with Kotlin. As an architecture, there were immutable data classes and functions (not methods which are members of a class), where the functions generate new immutable data objects from other immutable data objects. And it felt like the scope of unit tests was reduced to those functions only.

So, is my assumption right that immutability reduces the scope of unit testing?

Best Answer

Tests check that things that can go wrong don't go wrong. The more flexible a software system is, the more ways there are for things to go wrong. Conversely, removing flexibility from a system means that less things can go wrong, and as a consequence fewer things have to be tested to achieve a similar level of confidence.

For example, static type systems. A typesystem is an automated proof of correctness. The level of proven correctness depends on the language, e.g. Haskell can prove more properties than Java, which can often prove more properties than C. The properties of the program that are guaranteed by a type system do not have to be unit-tested, for example that a function returns a result of a particular type.

Immutability is another approach that decreases the flexibility of a system, but also makes it easier to reason about and therefore requires fewer tests. In particular, the state of a mutable object may change after it was constructed. These state changes may move that object into an invalid state. So a thorough test will have to check these state transitions and ensure that no illegal states can be reached. Consumers of this object will have to be tested that they can work with the object in any of its states. In contrast, the state of an immutable object cannot change once constructed. Therefore, there are no state transitions to test. If a consumer receives an instance, they always know it is in the single valid state for that class.

When testing a function, we want to assert that it has some desired effect. So we arrange a particular state, act out the test case by invoking the function, and assert that the state after the call has some expected properties. This is most easy when the function does not have any effect except for returning a value: when we have a pure function. Immutability is not a precondition for a pure function. But if that function is passed immutable parameters, we know that the function cannot change those parameters – one less thing that could go wrong. Note that for a method, the implicit this parameter is also an input parameter that may or may not be changed.

In practice, immutability is not always possible and not always desirable. In particular, the Java ecosystem does not have a mechanism to track whether values are immutable and whether a parameter may be changed (compare const in C++). So in Java, immutability and pure functions are more a convention than a type-system provable correctness property. Also, some functions may be effectively pure but not pure in the most literal sense, e.g. if they perform logging as a side effect. But that is entirely OK, as such functions are still effectively pure from a perspective of a caller.

In my experience, preferring immutable designs and mixing OOP with functional programming techniques leads to simpler, more reasonable systems. That we need fewer tests for such systems is not the main value, but just a consequence of a generally simpler system where fewer things can go wrong.