Over the weekend, I was thinking more about testing. I’ve been watching The Clean Code Talks, and these have helped me solidify more of my thoughts on testing.
Previously, I’ve written about bad ways to write tests. Now I’d like to start to talk about ways that you can make code that’s really difficult to test.
As I mentioned before, testing is essentially:
- Determine the required initial state required to test what you want to test
- Run the code you want to test
- Determine whether the final result matches what you expect
Of the three, the first one is the easiest to muck up, although the second one is pretty easy to sabotage too.
So, how do we make a class so that its initial state is prohibitive to testing?
Do a lot of work in the constructor
… especially work that depends on other classes.
By doing this, you make isolation much more difficult. If there’s an error calling __init__, it often turns into a challenging Whodunnit caper (with no returned object to inspect). You’re not only testing your class, but all of the classes it is creating.
Do work that depends on external entities
If building a large object graph isn’t enough, you can add network access to your constructor. Or database access!
Once you’ve got remote access in your object constructors, well… your unit tests will only run if you’ve got access to the remote resources as well.
This hurts you in several ways. For one, remember that the first step of testing is to set up the required initial state. If part of setting up the initial state involves logging in to a database server and flushing the rows from several tables, well, your unit tests are going to be painful to write. And maintain. And they’re going to be flaky. When a test fails, you’ll be wondering if you broke the code or if something happened to the shoestring development database server.
Additionally, this also affects running the code you want to test. It means that you can’t run your unit test suite from: an airplane, the passenger seat of a car, the beach, the boardroom with flaky wireless connectivity, your back deck (that day you decide to work from home), and so on.
Depend on global state
One of the most common pieces of global state I’ve seen is the “Configuration Object”. Whether based on ConfigParser, Django’s settings.py, the Windows Registry, or countless other frameworks, one this is common: these make things amazingly difficult to test.
Oh, I forgot environment variables too.
Here’s the problem: by changing the global configuration settings, you could be influencing the outcome of another test. Either you need to, at the start of each test, ensure that the global state is the way you want it (if that’s even possible), or you need to hope that you’re running before any other test that could have modified the global state.
How do I get around all this?
Without giving away too much, there’s a few different ways to address these. Mocker is one way (that I don’t have much experience with). Dependency Injection is another. This past weekend I started experimenting with DI, and from what I can tell, it really helps address all of these problems. That’ll be next time, I think.