The art of writing testable code is not as easy as it seams. There are a lot of things you need to keep in mind just in order to create code that is actually testable.
This is something we keep coming back to and probably something you encountered your self. “This code isn’t testable” is probably something you have told your self multiple times, when revisiting old, legacy code. And that is true sometimes. In this article, we will discuss some of the pitfalls when developing that you should avoid in order to make it as easy as possible to write tests for it. We will be focusing on unit tests here, since good integration tests are not usually that dependent on good code (unfortunately).
Code against abstractions
What I mean by this is that you should avoid using concrete implementations inside your code. Instead of calling GetCustomers() on a SQLRepository, you should call on IRepository instead. If you code against concrete implementations, you no longer can substitute those dependencies with implementations you control. You have to rely on that database or on that external service. What happens if it is down? Or what if you want to test how your code behaves if the database is unavailable? Do you call your DB admin and ask him to take it offline for a minute? Please don’t do that, btw. Coding against abstractions is the first step for writing testable code. The next on is:
Dependency Injection
In a previous article, I wrote that using dependency injection is a necessity in order to write testable code. I stick by those comments. Passing your dependencies through the constructor or through the method calls themselves are absolutely necessary in order to write testable code. Not only do you have to use abstractions, but they can’t be created inside the code you want to test. This is very important, because you need to control those dependencies and how they behave. While you can technically do this without dependency injection, you will end up with a code base full of pre-compiler macros.
#IF DEBUG
IRepository repo = new FakeRepo();
#ELSE
IRepository repo = new SQLRepository();
#ENDIF
This will make it very difficult to maintain. And what if you want that repository to return different things, depending on the test? Do you create multiple pre-compiler macros called DEBUG1, DEBUG2, DEBUG3? Just use dependency injection instead, and send those fake repos through the constructor.
Avoid side effects
Writing totally side effect code is not possible. Not only is it not possible, it doesn’t make sense. If your application isn’t affecting the world around it, why have you even created it? But seen in isolation, your classes can usually be side effect free. This makes it much easier to test, since you no longer need to modify it’s state before executing your tests. This is really problematic since those fields are usually private to the class itself. What do you do then? Do you open it up to the world as a public member in order to modify it before the test? Not only that, but you have to ensure that the class you are testing gets reset correctly after every test.
Avoid static members
Avoid static members in your code as much as humanly possible. This is because it is almost impossible to control the behavior of static members during test execution. I actually think that there are some paid solutions out there that allow you to do that, but why pay for something you can do for free?
Let’s try with a static member everyone who has written C# code has used, the Datetime.Now member. What if your code needs to change behavior during low-peak hours, like for example midnight to 01:00 AM? How will you test for that? Will those tests need to be run during midnight in order to validate your application? A much easier solution is send them as parameters to your method. Or even write small wrapper providers around those static members, which you can then inject into your application.
The authorization system we used at work was accessed through static members. You would for example get the user ID of whoever was logged in by calling
var user = Principal.UserID;
This made it really hard to test cases where we wanted to control access to specific resources. I ended up creating something similar to the following interface and class and injecting that into the application itself. I would then use that instead whenever I needed to control access to some resource.
public interface IAuthenticatedSession
{
int EmployeeID { get; }
int CompanyID { get;}
int AccessLevel { get; }
}
private class AuthenticatedSession
{
public int EmployeeID => Principal.EmployeeID;
public int CompanyID => Principal.CompanyID;
public int AccessLevel => Principal.AccessLevel;
}
This allowed me to control who was logged into the system during tests execution, and write tests to make sure that those mechanisms where working as intended.
Write small classes and methods
Finally, try to keep the scope of your classes and methods small. This makes it much easier to reason about what your code should be doing. This will also make the tests themselves much smaller, since there is much less you need to setup and test for. Smaller tests also make it easier to reason about what you are actually testing.
Conclusion
Writing testable code is not that hard, as long as you keep some of these tips in mind. I actually think that deciding what to test for is a lot harder than writing the code itself. My experience is that when you start writing code that adheres to the principals of dependency injection, your code almost magically starts becoming much more testable.
In my next article about testing, I will discuss how to write the tests themselves. If you can’t wait, I can recommend reading up on some tools and frameworks to make testing infinitely easier. Things like Moq and AutoFixture are dream tools for writing unit tests. Once you start using them, you will never understand how you did without them. At some point, I will go in depth with these two frameworks since they are just incredibly powerful.
I hope you enjoyed my article! Until next time.
2 thoughts on “Writing Testable Code, an Intro to Unit Testing”