The perils of sharing state when writing tests
Sharing state is always a bad idea. Functional programmers have been using immutability for a long time now, completely avoiding the perils this can bring.
For all the others that for one reason or another keep going on with the good ol’ OOP, this is an hard lesson that needs to be learned. And sooner is the better.
Moreover, this is true for tests as well. They are a piece of software too, and as such they deserve the same care and respect we give to the rest of our applications.
Let me say that again. Never. Share. State.
As usual Martin Fowler has a nice article about references and immutable objects. Make sure to check it out, I’ll wait.
Now let’s go back to the tests problem. The standard setup for unit tests is to:
- mock whatever dependencies your SUT has
- configure them
- instantiate the SUT
- test and assert
Now the problem lies in points 1 and 3. Well, might lie there.
<figcaption>I have seen things…</figcaption></figure>
There are cases where you might be tempted to declare the SUT and its dependencies as class members. This will surely simplify the setup as you can initialize everything in the test class cTor.
It’s tempting, I know. I’ve been there.
Maybe you want to get a step further and declare them as static. Why not, we’re reusing them and we don’t want to waste precious time instantiating stuff over and over.
Well if you do, keep in mind that those instances are shared, so every setup you do on the dependencies for example, will be reused by all the tests in that class.
Let’s say for example that you’re instructing a dependency to throw an exception in order to catch it in the SUT and handle it properly. You’re asserting this in a test, but what all the other tests? They’ll get the exception too.
I think you’re starting to see the point here.
As usual, I’ve created a micro-repository showing the idea. You can find it here.
All in all this might seem a minor thing, but trust me, sooner or later you’ll be in the situation where a test passes when executed alone and fails when you run all the suite. Now, that is a typical case of shared state. Keep an eye on the symptoms!
Personally, I’m a great fan of xUnit. It has some very nice features, but this is one of my favorites (directly from the docs) :
“xUnit.net creates a new instance of the test class for every test that is run, so any code which is placed into the constructor of the test class will be run for every single test”
This will basically save you as it will create a fresh context for every test.
Now of course this doesn’t apply much to persistence and integration tests. In that case you’re expecting a db to be provisioned and accessible. But again, in that case it would be much better if you rely on a Fixture (or whatever your library of choice has).
A much better strategy would be to generate the db dynamically for each test and drop it at the end. If you’re running on a CI/CD platform (like Gitlab or Travis), you can even spin up a Docker container with the db server. I might write a post one of these days and elaborate more.
Going back to the code, if you take a look at the BetterTests class you’ll see that I’m defining a static factory method for the SUT, but with a twist: it’s accepting “configuration functions” as arguments. The factory method will instantiate the mock dependencies, then use those functions to configure them with whatever is passed from the consumers.
So in one case, we’re building a dependency that throws, while in another we’re just accepting the instance and nothing else.
Pretty neat, isn’t it?
Don’t forget to take a look at my series on DDD and Entity Framework Core!