Testing code with heavy external dependencies using template method

Mar 8 · 4 min read
Never sure

Writing Unit Tests for a class seems to be a pretty simple task — you import class you want to cover with tests, you create its instance, call some method with fixed parameters and expect the result to be always the same value, on which you can assert. No big deal. Unfortunately, it’s not always the case. Sometimes even creating object requires to create other objects first (e.g. some config, or connections to external services, databases etc.), and THOSE objects often have their own dependencies to create too, in never-ending spiral of doom. In this article I’ll show you simple trick, which can turn this hell into a pleasure of watching test coverage increasing, with relatively small effort.

Let’s say you started work at a new project, not covered with any tests so far. Not long after you cloned the repo and started reading the codebase, your Project Manager asks you to add tests for this, recently written piece of code:

Ok, so let’s start. Apparently this class retrieves employee base salary from the database, then performs some logic buisiness calculations on this and returns actual value of salary after these transformations. It seems also that `db` is some global value, representing real database located somewhere on the remote server, and allowing to execute SQL queries against it.

What are our options? First thing that comes to mind is to actually connect to production database during tests, reusing the same object with the same config (possibly copying all the setup of that connection to tests). This solution has several very serious drawbacks:

  • We have to know what is actually stored in this database, choose some employee, check his base salary, and make assertions on his bonus value. But what if this employee gets a raise/gets fired? Our tests will unexpectedly fail with no obvious reason, and we’ll have to go back to them and fix it by checking database again, which costs us more and more effort as our tests grow bigger and more complex.
  • By making queries to database we load it with more traffic, if tests are complicated it can even slow down the actual application — very undesirable.
  • It costs time — connection to remote server will slow down our tests, which in turn will cause them to be executed rarely, turning them in a pile of unused code, which doesn’t really serves its purpose — it can’t detect errors early. Besides — what if database is unavailable due to some network issue? It stops us from running any tests until it’s restored — BAD
  • As already mentioned — we need to copy all database setup to our testing environment. What if e.g. credentials to the database changes? Again — costs of maintenance of these tests increase.

Second option? We could setup test database, which would be a stable replica of actual database in our local environment. Althought it’s a step in good direction, this solution still is not perfect. Do we really have to setup whole database to test one small method? There must be a better solution.

Indeed, one small trick can saves us a lot of time. It can isolate code from its external, heavy dependencies (it isn’t only about databases — it can be filesystem, server, third party service or literally anything). First, it requires a small refactoring in our code — but it’s so easy it shouldn’t break anything. Consider following change:

What did we actually do? We simply extracted everything related to getting access to an external dependency to separate method. Why? So we can subclass and redefine it for tests. Look:

By redefining this method, we broke dependency beetween tested class and db. This simple trick allows us to actually test our buisness logic instead of testing database connection, network, and everything else.

Moreover — let’s imagine db isn’t a global variable, but a class with static `connect` method — this trick still works, even thought our class depends on a concrete class. Even more — it works even if db is implemented as singleton, which is extremally difficult to patch during tests, and usually requires much more refactoring and some dirty “workarounds” to get it to work.

But to be honest — it’s still just a trick. Major problem is still about bad design. Depending on global variables, static methods of concrete classes, or singletons are nowadays considered antipatterns and should be avoided at all. Personally — in this case I would try to use dependency injection — I would make of the db constructor parameter of `EmployeeService` class, so I can pass a fake implementation to it during tests, instead of subclassing it. Here comes again the statement that good tests induce good design :)

Another issue noticed at first glance by an experienced developer in here is that we mix up low level details (db connections, tables and columns) with high level buisiness logic of calculating payments for employees. Breaking this responsabilities to different components would be a good idea, and surely would turn testing easier.

Putting above two paragraphs apart and assuming you don’t have time, money or possibility to make architectural changes, I believe it is the shortest and simplest way to put tests into their place. Then, perheps in some longer perspective, the possibility to make redesig will arise. And since we have tests, making these changes will be safer and hopefully will take less time and effort.

Oh, and by the way — by learning this little trick you learned also one of the object oriented design patterns — template method. Easy, wasn’t it? :)

This article was based on Michael Feathers’ great book — “Working Effectively with Legacy Code”. I recommend reading it to everyone who felt (even just a little) interested by the article above. Good luck and happy coding!

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade