Dependency Injection with Spring

Modified on Tue, 28 Sep, 2021 at 9:18 AM

TABLE OF CONTENTS

Introduction

The most popular use case of DI in the Java programming world would be undoubtedly the Spring framework. It’s widely popular and being used across various companies worldwide. Since the early 2000, it has been considered as a standard framework for application development. 

    At the core, Spring framework is a Dependency Injection Container, with a couple of convenience layers like service, controllers etc. It is responsible for creating and maintaining the object’s life cycle with few special annotations. You may have seen the annotations like @Autowired , @Service , @RestController & several others in various java classes in the java backend application code.


DI Annotation in Spring

The annotation @Autowired is used for injecting dependencies. This annotation is used for “auto-wiring” in the Spring. Autowiring is the process on which the framework figures out the dependencies of a Spring bean, instead of us explicitly specifying them. We can annotate fields and constructor using @Autowired to tell Spring framework to find dependencies for you.

    Let us see this with the example of the class CarFactory, which had a dependency on the class TyreFactory. In Spring, the only change that needs to be done is adding the annotation @Autowired before the dependent attribute. Rest all stays the same. It can be done in the following manner.


public class CarFactory {

   @Autowired
   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);
   }
}

The @Inject annotation also serves the same purpose. @Inject is a standard annotation defined in the javax library, whereas @Autowired is spring specific.


Milestone #1: @AutoWired in Unit Tests

Now, let’s experiment with Spring DI further using unit tests. Our earlier test class for TyreFactory now has a few more annotations added.


Car.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.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;

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

@ExtendWith(MockitoExtension.class)
@SpringBootTest(classes = Driver.class)
public class TyreFactoryTest {

   @Autowired
   private TyreFactory tyreFactory;

   @Test
   public void TestSedanTyre(){
       Tyre tyre = tyreFactory.getTyre(CarType.Sedan, 26, "rubber");

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

Notice the @SpringBootTest annotation for Spring boot application with the application class name specified, like this: @SpringBootTest(classes = Driver.class) . The Driver class has also been annotated with @SpringBootApplication to earmark this class as the Spring boot application class.

    If we miss the annotation @Autowired in this test case, it will lead us to Null pointer Exception when the code tries to access the instance of TyreFactory due to non-instantiation of the object.


Activity #1

Can you check, whether the instances created with  @Autowired creates a new object every time or are those singleton? 


Hint:

use System.identityHashCode() to your rescue. 


Follow-up Question:

What’s the advantage of doing so?


Answer:

Creating Singleton will save resources and time.


Activity #2

What happens if we remove the @autowired annotation for “TyreFactory” in the “TyreFactoryTest” class? 


Answer:

It will fail with a Null pointer exception as the object will not be initialized. This is one of the most common errors, where the Spring Boot application fails to start.


Milestone #2: Mockito & Spring

No, let’s see how we can use auto-wiring along with mocking dependencies. The test case we wrote earlier for CarFactory had dependency on the beans of CarFactory and TyreFactory. Here’s the new version of that test class.


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.InjectMocks;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
@SpringBootTest(classes = Driver.class)
public class CarFactoryTest {

   @MockBean
   private TyreFactory tyreFactory;

   @InjectMocks
   private CarFactory carFactory;

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

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

We can see @SpringBootTest here as well along with the application class name. Here, 2 new annotations are being used here: @MockBean & @InjectMocks.

    @MockBean is used for automatically creating the bean for the class for mocking. Here, as we are mocking the instance for TyreFactory to return our preferred tyre, we are using this annotation here.

    @InjectMocks is used for auto-creation of beans, for which dependencies need to be mocked. Thus, we are using this for the CarFactory instance. Rest of the code remains the same as     before.


Activity #3

Predict the output of the following code segments for the class CarFactoryTest.java. In case of failures, debug the test cases to zero down the exact reason of failure.


Code Segment #1

public class CarFactoryTest {

   @Autowired
   private TyreFactory tyreFactory;

   @InjectMocks
   private CarFactory carFactory;
   @Test
   public void verifyCarCategoryHatchbackMRF(){
       Car car = carFactory.getCar(CarType.Hatchback, "Steel Grey", "", 27, "rubber");

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


Answer:

It will fail with Null Pointer Exception as the instance of TyreFactory inside CarFactory’s object will not be initialized. @InjectMocks requires @MockBean for creating mock dependency objects.


Exception StackTrace:

java.lang.NullPointerException
  at org.crio.qcar.CarFactory.getCar(CarFactory.java:17)
  at org.crio.qcar.CarFactoryTest.verifyCarCategoryHatchbackMRF(CarFactoryTest.java:25)
      …

CarFactoryTest > verifyCarCategoryHatchbackMRF() FAILED
    java.lang.NullPointerException at CarFactoryTest.java:25
1 test completed, 1 failed


Activity #4

public class CarFactoryTest {

   @Autowired
   private TyreFactory tyreFactory;

   @Autowired
   private CarFactory carFactory;

   @Test
   public void verifyCarCategoryHatchbackMRF(){
       Car car = carFactory.getCar(CarType.Hatchback, "Steel Grey", "", 27, "rubber");

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

Answer:
  • It will pass the test case successfully, as it will take the Spring auto-wire route here. The change made here was replacing @InjectMocks with @Autowired
  • But here, it doesn’t mock the dependency. So, if we are to mock any method call of the TyreFactory class, we will not be able to do that.

Activity #5

Consider another test case for CarFactory like below.


public class CarFactoryTest {

   @MockBean
   private TyreFactory tyreFactory;

   @InjectMocks
   private CarFactory carFactory;

   @Test
   public void verifyCarCategoryHatchbackMichelin(){
       MichelinTyre michelinTyre = new MichelinTyre(27, "rubber");
       when(tyreFactory.getTyre(any(CarType.class), any(Double.class), any(String.class)))
               .thenReturn(michelinTyre);
       Car car = carFactory.getCar(CarType.Hatchback, "Steel Grey", "", 27, "rubber");

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

What will be the output once we run this test case?

Answer:

It will pass the test case, as we are returning Michelin tyre from the mocked TyreFactory in this case. This verifies that, indeed, the mocked class object is passed. 

    Follow-up, print the identity hashcode of “michelinTyre” in the method.

verifyCarCategoryHatchbackMichelin” and inside carFactory.getCar() to check, whether both are indeed the same object or not.


Activity #6

What will happen if we change the getCar() in CarFactory class to the following and run the last test case?


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


Answer:

The change made here was replacing the current object’s tyreFactory instance with a new instance of TyreFactory. 

The code will compile correctly, but the test run will fail, as it will not use the mocked TyreFactory and try to use the actual TyreFactory class’s object. The actual TyreFactory object will return MRF Tyre for a hatchback car and thus the test case will fail. This is one of the common mistakes developers do, that leads to test failure.


Key Takeaway

In this section, we’ve learned about the @Autowired annotation, where to use it and where we shouldn’t. We have also seen examples of writing test cases with auto-wiring and some common mistakes.

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