Dependency Injection in Unit Testing

Modified on Fri, 24 Sep, 2021 at 9:07 AM

TABLE OF CONTENTS


Introduction

So far, we have discussed what Dependency Injection is and how we can do it in Java. Now, let us see why it's such a big deal in the Java world through its use cases. Unit testing or writing automated test cases for testing a small unit of code is one of the most popular use cases of Dependency Injection apart from it following the coding standards.


Let us go a little back in the history and revisit option #1 for creating a constructor for the class Car.


Car.java

public Car(CarType type, String color, String model, double tyreDiameter, String tyreMaterials) {
   this.type = type;
   this.color = color;
   this.model = model;
   if(type == CarType.Sedan){
       this.tyre = new MichelinTyre(tyreDiameter, tyreMaterials);
   }
   else{
       this.tyre = new MRFTyre(tyreDiameter, tyreMaterials);
   }
}


Here, if we go with this option and would like to test the Car class related to a specific implementation of Tyre, we will not be able to do it directly as there’s no option to change the Tyre implementation to be used by the Car class. 


In several cases, we will be writing test cases of a code segment, where external data stores are involved. In such cases, we will have a dependency on a method of the Data Access Object(or DAO) and inside the current method, there will be one call to a DAO method to collect the data. Let us take a look at the following example.

public List<Car> getSedansWithOddNumber(){
   List<Car> sedanCars = carsDAO.getCarByType(CarType.Sedan);
  
   return sedanCars
           .stream()
           .filter( car -> car.getSerialNo() % 2 == 1)
           .collect(Collectors.toList());
}

Let’s add a unit test, we would like to test whether the method getSedansWithOddNumber() actually returns cars with odd serial numbers or not. We will never be able to test the logic written in this method individually as there’s always a dependency on DAO to get the list of cars. In order to be able to write the test case, we will need to make sure that carsDAO.getCarByType(...) should return a list of cars, where we will know the serial numbers beforehand.  The way to do this is to mock the DAO class and return a list of our choice.  Dependency injection helps us here, as it enables us to replace the actual version of carsDAO with the mocked version.


Let us see further how we can write our own unit Tests using Mockito & JUnit. We will be using the QCar project for example.


Note:

If you’d like to know more about Mockito & JUnit, please checkout the Mockito & Junit bytes.


Milestone #1

Since the last version of QCar project, we have made few changes in various classes and you can get the latest version here.


Car.java

package org.crio.qcar;

import lombok.Getter;
import lombok.ToString;
import org.crio.qcar.tyres.Tyre;

@Getter
@ToString
public class Car {

   private CarType type;
   private String color;
   private String category;
   private Tyre tyre;

   public Car(CarType type, String color, String model, Tyre tyre) {
       this.type = type;
       this.color = color;
       // Pattern of category name:   <car_type>-<tyre_manufacturer>-<model_number>
       this.category = type.toString()+ "-" + tyre.getManufacturer().toString() + "-" + model;
       this.tyre = tyre;
   }

   public void drive(){
       tyre.move();
   }
}


Tyre.java

package org.crio.qcar.tyres;

import lombok.Getter;
import org.crio.qcar.Manufacturer;
@Getter
public abstract class Tyre {
   private double diameter;
   private String materials;
   private Manufacturer manufacturer;

   public Tyre(double diameter, String materials, Manufacturer manufacturer) {
       this.diameter = diameter;
       this.materials = materials;
       this.manufacturer = manufacturer;
   }

   public abstract void move();
}


MRFTyre.java

package org.crio.qcar.tyres;

import lombok.ToString;
import org.crio.qcar.Manufacturer;

@ToString
public class MRFTyre extends Tyre{

   public MRFTyre(double diameter, String materials) {
       super(diameter, materials, Manufacturer.MRF);
   }

   public void move(){
       System.out.println("Started moving with MRF...");
   }
}


TyreFactory.java

package org.crio.qcar.tyres;

import org.crio.qcar.CarType;

public class TyreFactory {

   public Tyre getTyre(CarType type, double tyreDiameter, String tyreMaterials){
       Tyre tyre = null;
       if(type == CarType.Sedan){
           tyre = new MichelinTyre(tyreDiameter, tyreMaterials);
       }
       else{
           tyre = new MRFTyre(tyreDiameter, tyreMaterials);
       }
       return tyre;
   }
}


CarFactory.java

package org.crio.qcar;

import lombok.AllArgsConstructor;
import org.crio.qcar.tyres.Tyre;
import org.crio.qcar.tyres.TyreFactory;

@AllArgsConstructor
public class CarFactory {

   private TyreFactory tyreFactory;

   public Car getCar(CarType type, String color, String model, double tyreDiameter, String tyreMaterials){
       Tyre tyre = tyreFactory.getTyre(type, tyreDiameter, tyreMaterials);
       return new Car(type, color, model, tyre);
   }
}

Now, let’s start with writing unit tests for CarFactory


Let’s write a unit test to test whether the car category starts with “Hatchback-MRF” for hatchback cars.


As the class CarFactory has an external dependency on TyreFactory, we will need to mock the instance of TyreFactory by using @Mock annotation of Mockito. When we use @Mock annotation, Mockito injects a mock object there. Also, the mocked instance should return a MRF Tyre when asked for a tyre. We can write it in this manner.


CarFactoryTest.java

package org.crio.qcar;

import org.crio.qcar.tyres.*;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
public class CarFactoryTest {

   @Mock
   private TyreFactory mockTyreFactory;


   @Test
   public void verifyCarCategoryHatchbackMRF(){
       when(mockTyreFactory.getTyre(any(CarType.class), any(Double.class), any(String.class)))
               .thenReturn(new MRFTyre(27, "rubber"));
       CarFactory carFactory = new CarFactory(mockTyreFactory);
       Car car = carFactory.getCar(CarType.Hatchback, "Steel Grey", "abcd", 27, "rubber");

       assertTrue(car.getCategory().startsWith("Hatchback-MRF-"));
   }
}


Activity #1

Can you write another test case, to verify that the car category name’s tyre manufacturer segment should always be the same as the injected tyre’s manufacturer, irrespective of the car type. Take an Sedan car and inject MRF tyre dependency to verify this. In that case, the category should start with "Sedan-MRF-".

Solution:


@Test
public void verifyCarCategorySedanMRF(){
   when(mockTyreFactory.getTyre(any(CarType.class), any(Double.class), any(String.class)))
           .thenReturn(new MRFTyre(27, "rubber"));
   CarFactory carFactory = new CarFactory(mockTyreFactory);
   Car car = carFactory.getCar(CarType.Sedan, "Steel Grey", "abcd", 27, "rubber");

   assertTrue(car.getCategory().startsWith("Sedan-MRF-"));
}

Activity #2

Can you check what will happen, if we execute the following code?


CarFactoryTest.java

package org.crio.qcar;

import org.crio.qcar.tyres.*;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
public class CarFactoryTest {

   private TyreFactory mockTyreFactory;

   @Test
   public void verifyCarCategoryHatchbackMRF(){
       when(mockTyreFactory.getTyre(any(CarType.class), any(Double.class), any(String.class)))
               .thenReturn(new MRFTyre(27, "rubber"));
       CarFactory carFactory = new CarFactory(mockTyreFactory);
       Car car = carFactory.getCar(CarType.Hatchback, "Steel Grey", "", 27, "rubber");

       assertTrue(car.getCategory().startsWith("Hatchback-MRF-"));
   }
}


It will throw a Null pointer exception like this:


java.lang.NullPointerException
  at org.crio.qcar.CarFactoryTest.verifyCarCategoryHatchbackMRF(CarFactoryTest.java:18)
  ...
  at java.util.ArrayList.forEach(ArrayList.java:1257)
  at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
  ...

CarFactoryTest > verifyCarCategoryHatchbackMRF() FAILED
    java.lang.NullPointerException at CarFactoryTest.java:18


Let us take a pause to figure out what’s happening here?

The code give a Null pointer exception in the following line:


when(mockTyreFactory.getTyre(any(CarType.class), any(Double.class), any(String.class)))


This gives us a hint that mockTyreFactory wasn’t initialized and thus threw NPE when calling the method getTyre(). If we compare this with the code provided earlier, we can see there's one thing missing in the new one: the @Mock annotation above the declaration of mockTyreFactory.


Note:

When we add the @Mock annotation in mockito, the framework automatically injects a mocked instance of the class.


Milestone #2

Let’s write unit tests for the class TyreFactory. Here’s an example of a test, where we can verify whether for Sedan, we get Michelin tyre or not.


TyreFactoryTest.java

package org.crio.qcar;

import org.crio.qcar.tyres.Tyre;
import org.crio.qcar.tyres.TyreFactory;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.junit.jupiter.api.Assertions.assertEquals;

@ExtendWith(MockitoExtension.class)
public class TyreFactoryTest {

   private TyreFactory tyreFactory = new TyreFactory();

   @Test
   public void TestSedanTyre(){

       Tyre tyre = tyreFactory.getTyre(CarType.Sedan, 26, "rubber");

       assertEquals("Michelin", tyre.getManufacturer().toString());
   }
}

Notice that, here we haven’t used dependency injection for writing unit tests. As the class TyreFactory doesn’t have any external dependency, DI is not required here.


Activity #2

Can you add the test case for Hatchback in a similar manner?


Solution:


@Test
public void TestHatchBackTyre(){

   Tyre tyre = tyreFactory.getTyre(CarType.Hatchback, 26, "rubber");

   assertEquals("MRF", tyre.getManufacturer().toString());
}

Key Takeaway

When to use DI in unit Tests?

We often write test cases for a single development code class in a single test code class. We’ve created a “CarFactoryTest” class to write the test cases for the class “CarFactory”. Now, we need a real object of the class “CarFactory” to write test cases for that. So, there we can’t mock. The class “CarFactory” has dependency on another class TyreFactory‘s object. Thus we need to mock the dependency here. That’s the reason, we have mocked “TyreFactory” in “CarFactory”, whereas we have created the actual object of “CarFactory” in the test method “verifyCarCategoryHatchbackMRF” like shown below.

CarFactory carFactory = 
new CarFactory(mockTyreFactory
);



Was this article helpful?

That’s Great!

Thank you for your feedback

Sorry! We couldn't be helpful

Thank you for your feedback

Let us know how can we improve this article!

Select at least one of the reasons
CAPTCHA verification is required.

Feedback sent

We appreciate your effort and will try to fix the article