Unit Testing and Dependency Injection

Unit testing is challenging when you add frameworks around your tests subjects.

Dependency injection is one of those patterns supported by frameworks, that we usually need in any project.

In this post, we are going to see the different ways to inject dependencies into our classes and how those injections impact our unit testing strategy.

NOTE: The following examples will use Spring as a dependency injector, however, the concept we are going to see is traversal to any dependency injection framework, for instance JavaEE/JakartaEE.

TRY IT YOURSELF: You can find the source code of this post here.

Unit Testing Series

Property Injection

Well, when you use property injection, the dependency injector needs to use reflection to access those properties and inject the right value.

The following example just call a URL to save a User using the RestTemplate object:

@Component
class Service {
  @Autowired
  private RestTemplate restTemplate;
  @Value("${service.url}")
  private String url;

  public User save(User user){
    return restTemplate.postForObject(url, user, User.class);
  }
}

As we can see, we tell Spring to inject the RestTemplate object and the value property service.url into the class attributes.

This implementation seems pretty simple and clean, however, this has consequences when we create unit tests:

@RunWith(MockitoJUnitRunner.class)
public class ServiceTest {
  @Mock
  private RestTemplate restTemplate;

  private String url = "http://test.url.com/";

  @InjectMocks
  private Service service;

  @Before
  public void before() {
    ReflectionTestUtils
            .setField(service, "restTemplate",
                    restTemplate);
    ReflectionTestUtils
            .setField(service, "url",
                    url);
  }

  @Test
  public void save() {
    User user = new User("id", "name");

    when(restTemplate
            .postForObject(url, user, User.class))
            .thenReturn(user);

    User result = service.save(user);

    assertThat(result).isEqualTo(user);
  }
}

First, let’s understand something about Mockito annotations:

  • @RunWith(MockitoJUnitRunner.class): Tells JUnit to run using Mockito runner
  • @Mock: Creates a mock class for that kind of object
  • @InjectMocks: Creates an instance of that object (not a mock) and tries to initialize it using the @Mock objects previously declared.

As we have no constructor in our Service class, @InjectMock just create the Service using its default constructor.

Now, we need somehow to “inject” our URL and RestTemplate mock to our tests subject, so, in lines 13 and 16, we use Reflection to get into the Service object and inject by force those values.

NOTE: You might ask “What do you mean by force?”. Well, our Service attributes are private, that means, we cannot access them outside of your Service class…. unless, we use Reflection to break encapsulation in our tests. If you need to use Reflection to access something in your production or test code… that’s a smell.

Besides, take into account that there is nothing in compile/startup time that tells us that Service class requires an URL and a RestTemplate. Let’s see what behavior Spring has if we remove the @Value("${service.url}"):

@Component
class Service {
  @Autowired
  private RestTemplate restTemplate;

  private String url;

  public User save(User user){
    return restTemplate.postForObject(url, user, User.class);
  }
}

Spring starts successfully:

Removing the @Value from property doesn’t affect Spring start up

Whatttt!!! Spring didn’t realize we won’t have an URL to call, this means, we only will see the problem, when the application is up and we execute that specific logic.

Setters Injection

Well, let’s avoid using Reflection to break our classes. So, we realized we can inject properties using setters methods, then, we changed our Service class:

@Component
class Service {
  private RestTemplate restTemplate;
  private String url;

  public User save(
          User user){
    return restTemplate.postForObject(url, user, User.class);
  }

  @Autowired
  public void setRestTemplate(
          RestTemplate restTemplate) {
    this.restTemplate = restTemplate;
  }

  @Value("${service.url}")
  public void setUrl(String url) {
    this.url = url;
  }
}

Now, our Spring annotations are in the setters methods.

How do our test look like now? let’s see:

@RunWith(MockitoJUnitRunner.class)
public class ServiceTest {
  @Mock
  private RestTemplate restTemplate;

  private String url = "http://test.url.com/";

  @InjectMocks
  private Service service;

  @Before
  public void before() {
    this.service.setRestTemplate(restTemplate);

    this.service.setUrl(url);
  }

  @Test
  public void save() {
    User user = new User("id", "name");

    when(restTemplate
            .postForObject(url, user, User.class))
            .thenReturn(user);

    User result = service.save(user);

    assertThat(result).isEqualTo(user);
  }
}

As we can see, we changed our awful Reflection code by setters.

We just remove a smell…. but, are those setters a smell too? yes, they are. You have a Service class with setters. Those setters are there only by tests / injection proposes, nothing more, that means, you are coupling your Service class to your test and injection framework.

And what if we remove the @Value("${service.url}") form the Service class?

@Component
class Service {
  private RestTemplate restTemplate;
  private String url;

  public User save(
          User user){
    return restTemplate.postForObject(url, user, User.class);
  }

  @Autowired
  public void setRestTemplate(
          RestTemplate restTemplate) {
    this.restTemplate = restTemplate;
  }


  public void setUrl(String url) {
    this.url = url;
  }
}

Spring starts successfully:

Removing the @Value from setter doesn’t affect Spring start up

Well, setters don’t solve this problem either.

And setters arise a new one, what if I changed the state of the Service object? for instance, this specific logic needs a different URL, so:

@Component
class Client {
  private Service service;

  public void anotherBusinessLogic(){
    this.service.setUrl("http://myotherurl");

    //TODO more logic
  }

  @Autowired
  public void setService(
          Service service) {
    this.service = service;
  }
}

Now, we have a Client class that sets http://myotherurl our Service URL attribute…. that makes sense? well, it might, but, your Service class wasn’t built for that propose. Could that happen? yeah, your code allows to happen.

NOTE: Spring creates singleton beans by default, that means, if you set http://myotherurl as URL, any client of the Service object, will see that value, and that won’t work in all clients, unless, each client sets its own URL to get that working…. however, that’s risky, remember, your are in a multi-thread environment sharing a singleton object that changes its states out of control….

As our Service class is mutable, this creates more troubles.

Constructor Injection

Remember, we are in an Object Oriented language, so, let’s use encapsulation to solve this adding a required constructor in the Service class.

@Component
class Service {
  private final RestTemplate restTemplate;

  private final String url;

  Service(RestTemplate restTemplate,
          @Value("${service.url}") String url) {
    this.restTemplate = restTemplate;
    this.url = url;
  }

  public User save(
          User user){
    return restTemplate.postForObject(url, user, User.class);
  }
}

As we can see, now our attributes are final!! as we created a required constructor. Our Service class is immutable now, that means, you cannot change its state after you create an object.

NOTE: For more information about immutability, you can see Object Immutability: The Basics

Our required constructor builds the Service object using what we need, the RestTemplate and the URL.

Now, let’s see how the unit testing looks like:

@RunWith(MockitoJUnitRunner.class)
public class ServiceTest {
  @Mock
  private RestTemplate restTemplate;

  private String url = "http://test.url.com/";

  private Service service;

  @Before
  public void before() {
    this.service = new Service(restTemplate, url);
  }

  @Test
  public void save() {
    User user = new User("id", "name");

    when(restTemplate
            .postForObject(url, user, User.class))
            .thenReturn(user);

    User result = service.save(user);

    assertThat(result).isEqualTo(user);
  }
}

As we can see, we used the Service constructor to build a new Service object using the RestTemplate mock and the URL. Looks cleaner and easy to understand now.

NOTE: Notice that we removed the @InjectMocks, sadly, Mockito doesn’t support to inject non mocks objects, that means, the URL cannot be injected to create the Service object. However, if your constructor arguments can be all mocks, you can use the annotation and remove the @Before logic, Mockito will do all the work for you.

Now, let’s remove the @Value("${service.url}") form the Service class.

@Component
class Service {
  private final RestTemplate restTemplate;

  private final String url;

  Service(RestTemplate restTemplate,
          String url) {
    this.restTemplate = restTemplate;
    this.url = url;
  }

  public User save(
          User user){
    return restTemplate.postForObject(url, user, User.class);
  }
}

Spring fails at start up:

Removing the @Value from constructor does affect Spring start up

Well, Spring fails to boot up because tried to build the Service object using the constructor, found the RestTemplate bean, but, Spring doesn’t know what String url means, so, it fails.

REMEMBER: You should decouple your business logic from the IoC container, for instance, using @Bean in Spring or @Produce in JakartaEE. You can read more here Factory Methods: Decoupling the IoC Container from the Abstraction

Now, we have an encapsulated, immutable Service class, easy to test and readable.

Final Thought

Property injection is a straightforward option, but, you will need to use Reflection to tests.

Setters injection solves the Reflection smell, but, open the door to code created only for injection and testing proposes, besides, the risk on changing its state in a multi-thread environment.

Constructor injection is the more stable option. The objects can be immutable, the unit tests are clean to build and the Dependency Injection frameworks usually fail at start up to unknown arguments.

If you liked this post and are interested in hearing more about my journey as a Software Engineer, you can follow me on Twitter and travel together.

4 comments

Leave a comment