Legacy Code to Testable Code #10: Getting Rid of Static Constructors


Here is the rest of the gang:

The Legacy Code To Testable Code Series

General patternsAccessibilityDealing with dependenciesAdvanced patterns
IntroductionAdd settersExtract methodStatic constructors (initializers)
RenamingMore accessorsExtract classMore static constructors
Add overloadIntroduce parameterInstance constructors
Testable objectConditionals to guard blocks

This time we’re going to tackle the most problematic issue in testing object-oriented languages. Obviously because constructors only exist in those, but also, because even power tools have problems handling them. But we’ll start where it hurts less.

The evil static constructor

Static constructors or initializers are not evil inherently. Languages just give us a way to call code implicitly, and that is the root of the problem. When the code is called implicitly, we run into problems when we want to go around it. There’s a reason that using TDD, you’ll find less of them lying around – In TDD you leave less implicit, and go more explicit.

As long as static constructors contain simple code, there’s usually no problem – we don’t care if they are called. What happens if they do more?

Regular mock-by-inheritance frameworks can’t deal with static methods. Unlike instance methods, they cannot be marked virtual and therefore overridden. Since all mock-by-inheritance frameworks override virtual methods, they cannot deal with those.

In C++ the trick can be applied with macros or similar at compile or link time, but not at run-time. Power tools in C++ solve the problem before it happens. While the refactoring patterns apply there too, let’s try to understand the complexity of Java and .Net.

Power tools in these languages don’t override methods, but modify the original implementation. They change the byte-code so the original methods are not called, and instead insert hooks into the code that will be called instead.

Since this mechanism works for any method, it works for static methods and initializers as well. However, modifying their behavior is a bit different, because of they are called implicitly by the virtual run time.

They are called before any method on the type are called (static or not), invoking static initializers across he type hierarchy, and are guaranteed to be called only once. Power tools, like PowerMock or Typemock Isolator, go to a great deal of effort in order not to break these rules, so the byte code doesn’t get corrupted.

They still can’t guess all kinds of run-time optimizations that the compiler can think of, and that leaves the rest of the work to us. There’s also a problem with the dependency of our tested code in what was initialized in them. Our tests will need to compensate for the now mocked state for the code-under-test. The tests grow in size, and get more coupled to the code.

The bigger issue is when a static initalizer is called, and it invokes calls to instance types, or base types, some implicitly. In order to suppress the current initializer we’ll need to suppress everything in the type hierarchy in order to get everything under control. More setup and more coupling.

The final problem is that static constructors are supposed to run only once. What does it mean for different tests, that run in different order? We need to make sure we’re testing in the right conditions that production code will run.

Tricky bunch. The “evilness” we usually refer to in static constructors refer to how much work we invest in going around them when testing.

Save us from evil

Obviously one way is to suppress them with power tools. However, like we’ve seen, we need to work hard in order to setup the system correctly. In addition, since mocking increases coupling between tests and code, mocking static initializers does so even more. We’ll need to understand the entire type hierarchy and dependency of the mocked types.

Refactoring can help, but we need to be careful. In order to get rid of the implicit initialization code, we need to move that code somewhere else. The idea is to move initialization code into the instance, and make it explicit in order to control it.

Let’s break down what to move where:

  • Calling methods on other types, or setting a state for them

This is the easy to do. We can move this code to another object we can call explicitly from somewhere else (Extract Method or Extract Class). It maybe tempting to move it to the instance constructor of our code-under-test. We’ll see why when we tackle instance constructors.

  • Initializing static fields on the current type

We use static fields exist to be accessible by all instances of our types. As such they can be relocated to another object, that can be made accessible to our object under test. Another option is to make these fields accessible to change from the calling code or tests. We can also extract the code to an initialization method, that can be called explicitly. These interfaces allow us to control the of the shared state for different tests.

  • Initializing objects of the current type

Enter the evil singleton. Cue the imperial march.

But singletons deserve more respect. We’ll continue this with examples next time.

Leave a Reply

Your email address will not be published. Required fields are marked *