If you are new to testing, this is an important concept to understand.

Legacy codebases often suffer from the fact that they are inherently untestable from a unit test point of view. The only way to test such code without refactoring, is integration testing, where possible, and End to End testing.

Dependencies and tight coupling

In order to write proper units tests, your components should be “loosely coupled”. Broadly speaking, this means that a function or method should not depend on a another component.

Very crudely speaking, this means that a Component A should not instantiate another component Component Busing the new keyword.

Instead, you would inject Component B into Component A in the class constructor, as a method parameter or using a Dependency Injection framework.

Problem - Tightly coupled code

To help visualise this, let’s take a simple example. To keep things clear, we’ll have everything in a single file. This is a simulation of a REST API where the controller gets a userID and asks a service to process it. The service in turn asks a database/repository function to get the user name from the database.

  1. Instantiation of the user service happens in the userController() method. This makes the UserService class a hard dependency of userController().

  2. Instantiation of the database service happens in the getUser() method. This makes the UserDB class a hard dependency of userService().

The above code works just fine. But it’s not “testable”.

We cannot write a unit test for the userController() method as a unit as it depends on UserService. The same applies to the user service method getUser().

Solution - Loose coupling

The solution to this problem is to inject the dependencies.

It is generally considered good practice to adhere to the SOLID principles in software development. The solution in our case is the I in Solid - Inversion of Control. We don’t want to go down the SOLID rabbit hole here, as many readers will already be familiar with it and it’s beyond the scope of this discussion.

Skipping to the solution then, the answer lies in instantiating any dependencies in some central location or to use a Dependency Injection framework, and then passing those dependencies as parameters wherever they’re required.

Here’s the refactored code.

  1. Instantiate the UserDB class in the controller's constructor.

  2. Instantiate the UserService class, passing in userDb as a parameter.

  3. Whenever the userController() method is called, it uses the previously instantiated userService.

  4. The UserService constructor is passed the userDb variable that was instantiated by the controller.

  5. getuserFromDB() now uses userDb whenever the getUser() method is called from the UserService.

Again, there are many ways to inject dependencies in practice such as Dependency Injection Frameworks, passing dependencies directly to methods and more. At this stage, the important thing to understand is the concept of loose coupling.

Why it’s now unit testable

Before

If we look at the above getuser() method, what is it that allows us to now write a unit test when we couldn't before?

The whole purpose of a unit test is to test one unit (in this case our method getUser() in total isolation.

However, UserDB userDb = new UserDB() prevents us from doing this because it is going to instantiate a UserDB object and then call its getUserFromDB() method. This in turn will invoke a database operation.

It's very much not isolated.

After

The loose coupled version looks like this.

Here, userDb was previously instantiated and passed into UserService in its constructor, so when we call getUser(), userDb is ready to go.

But that still doesn’t really answer the question of why this is more testable.

Mocking

The answer lies in what happens when we write a test.

Mocking, made easier with Mocking Frameworks such as Mockito in Java and Moq in C#, allows us to inject a fake or dummy object whose behaviour we can fully control. Take a look at the unit test below.

  1. First, we arrange or setup what we need to run the test. In our case, we want to create a mock of the UserDB class. If we were following strict TDD, we would mock against an interface IUserDB rather than UserDB class itself. If you're doing Test Driven Development (TDD) then using an interface has the advantage that you can test before even writing the implementation of UserDB. Don't worry about this for now.

  2. Next, we act. when(mockUserDb.getUserFromDB(any())).thenReturn(expected). This tells our mocking framework “when getUserFromDB() is called, with any parameter, from UserService, then intercept it and replace it with something that returns expected (=”John Brook”). We can then call getUser() and rely on it return what we want.

  3. Finally we assert that what userService.getUser() returns is what we expect, expected.

Seems like a lot of work!

In our example, where our service is not doing anything really, you could consider whether it is really worth testing. However, real world examples are rarely this simple. As soon as there is some sort of business logic in your method, there will be things that are absolutely worth testing. The small amount of effort is well worth the future gains in quality.

Conclusion

We hope we’ve helped make a bit clearer how making your code testable is the first step on the road to great quality.

DevMate helps reduce the amount of work required to generate your tests whether you’re using TDD or not. Developers can involve Domain Experts, PMs, POs and QA Engineers in defining requirements and test cases as well as generating the test code. This lets developer spend less time writing test code and more time writing functional code.