jeudi 3 janvier 2019

Testability trick - Replace static initialiser with lazy initialiser

A simple 4-step refactoring to make code more testable.

Static initialisers are a major hurdle for testing. If you have a static initialiser that for instance performs a request to the database then you cannot access that class in a unit test. Let alone mock it. In fact only importing this class will execute the query. So even if you're trying to test some other class, you cannot, since the offending class is contaminating every import of it with non testability. You want to make sure to remove the static initialiser so that other classes can be tested in isolation.

The solution is to replace the static initialiser with a lazy initialiser. This will allow you to load the class for mocking. You'll probably have to also replace a static method by an instance method in order to avoid calling the real method and so properly test classes that are calling the offending class, but first things first.

Assuming that your static initialiser is initialising a static field of your class:
  1. Encapsulate the access to the field with a simple getter
  2. Use the *Extract Method* refactoring to extract all the code from the static initialiser
  3. In the getter initialise the field *if it is null* by calling the extracted method 
  4. Delete the static initialiser
Yep it's that simple! :o)

Your next step will likely be to replace the public methods using this getter by instance methods so that those can be tested (example).

Let's have a look at an example of the code before refactoring

And now here's the refactored code.

Having "Replaced static initialiser with lazy initialiser" we're almost there. We can safely import this class, but we can't yet mock the static method. In order to make it mockable we can simply add an instance method as a proxy to the static method, then use the instance method instead of the static method .

1 commentaire:

  1. Great trick indeed.

    I've used it countless of times. When mocking this kind of global variables though, we need to be careful to remove the mock after the test. I've used some JUnit Rules, C++ RAII objects or other tricks in other languages to make sure I don't forget. I might write about this on my own blog some day. Thanks for the idea!

    NB: I cannot see the code *after* the refactoring, maybe something went wrong.