Bootiful Test Driven Development

Share on:

Software engineers have been ardently following Test Driven Development (TDD) as an XP practice for having necessary safety nets. I have even tried covering different schools of TDD with an example in one of my previous posts. Considering recent surge in using Spring Boot for developing Microservice applications, I felt a need to understand and learn how to do TDD whilst implementing Spring Boot application.

In order to understand how Spring Boot simplifies the overall process of doing TDD, we will consider a very simple use case -

  1. Given a reservation system, when user enters details of the user which needs to be searched, it fetches required user details i.e. First name and Last Name
  2. Extend above use case to ensure that caching is being used to optimize lookup operation

End to End Test - 1st Version

By following Outside-In / Mockist school of TDD, we will start with an end to end test - Needless to say that it will fail initially. Of course we will need to ensure that it is compilation error free.

 1package com.its.reservation;
 2
 3import org.assertj.core.api.Assertions;
 4import org.junit.Test;
 5import org.junit.runner.RunWith;
 6import org.springframework.beans.factory.annotation.Autowired;
 7import org.springframework.boot.test.context.SpringBootTest;
 8import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
 9import org.springframework.boot.test.web.client.TestRestTemplate;
10import org.springframework.http.HttpStatus;
11import org.springframework.http.ResponseEntity;
12import org.springframework.test.context.junit4.SpringRunner;
13
14import com.its.reservation.repository.Reservation;
15
16@RunWith(SpringRunner.class)
17@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
18public class ReservationEndToEndTest {
19	@Autowired
20	TestRestTemplate testRestTemplate;
21	
22	@Test
23	public void getReservation_shouldReturnReservationDetails() {
24		// Arrange
25		
26		// Act
27		ResponseEntity<Reservation> response = testRestTemplate.getForEntity("/reservation/{name}", Reservation.class, "Dhaval");
28		
29		// Assert
30		Assertions.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
31		Assertions.assertThat(response.getBody().getName()).isEqualTo("Dhaval");
32		Assertions.assertThat(response.getBody().getId()).isNotNull();
33	}
34}
@SpringBootTest 

Annotation that can be used for testing Spring Boot application. Along with Spring's TestContext framework it does following -

  • Registers TestRestTemplate / WebTestClient that can be used for running application
  • Looks for @SpringBootConfiguration for loading spring configuration from the source. One can not only have custom configuration but also alter the order of configurations
  • Supports different WebEnvironment modes

Presentation Layer

Next we start with unit tests which will eventually help us in successfully executing the above end to end test. Since we have adopted Outside - In approach, we will first start with unit testing of REST endpoint. We will be strictly adhering to one of the fundamental rule of TDD

"No Production code without any test"

So in order to make end to end test pass, we will need to have missing pieces. The first missing piece that we will require is the API endpoint i.e. REST controller. We will first start with the controller test

 1import org.junit.Test;
 2import org.junit.runner.RunWith;
 3import org.springframework.beans.factory.annotation.Autowired;
 4import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
 5import org.springframework.test.context.junit4.SpringRunner;
 6import org.springframework.test.web.servlet.MockMvc;
 7import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
 8import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
 9
10import com.its.reservation.web.ReservationController;
11
12@RunWith(SpringRunner.class)
13@WebMvcTest(ReservationController.class)
14public class ReservationControllerTest {
15	@Autowired
16	private MockMvc mockMvc;
17	
18	@Test
19	public void getReservation_shouldReturnReservationInfo() {		
20		try {
21			mockMvc.perform(MockMvcRequestBuilders.get("/reservation/Dhaval"))
22				.andExpect(MockMvcResultMatchers.status().isOk())
23				.andExpect(MockMvcResultMatchers.jsonPath("firstName").value("Dhaval"))
24				.andExpect(MockMvcResultMatchers.jsonPath("lastName").value("Shah"));
25		} catch (Exception e) {
26			// TODO Auto-generated catch block
27			e.printStackTrace();
28		}
29	}
30}

In order to run the test, above test has to be made free of any compilation error. So this leads us to creation of actual endpoint.

 1package com.its.reservation.web;
 2
 3import org.springframework.web.bind.annotation.PathVariable;
 4import org.springframework.web.bind.annotation.RequestMapping;
 5import org.springframework.web.bind.annotation.RequestMethod;
 6import org.springframework.web.bind.annotation.RestController;
 7
 8import com.its.reservation.repository.Reservation;
 9
10@RestController
11@RequestMapping("/reservation")
12public class ReservationController {
13	@RequestMapping(method = RequestMethod.GET, value = "/{name}")
14	private Reservation getReservation(@PathVariable String name) {
15		return null;
16	}
17}

Now that we have an endpoint, we will be able to execute test case and we are sure that it is going to fail :) as it is not returning null currently.

Since the Controller is going to further delegate its task of business processing to the underlying Service. We are able to see that Outside-In / Mockist approach of doing TDD is helping us to determine collaborators required by class under test. Hence we will be creating ReservationService with required API which will be devoid of any behavior for time being - As its actual behavior will be discovered when we start writing unit test case for ReservationService. So we create the collaborator to get our class under test i.e. ReservationController compilation error free

 1package com.its.reservation.service;
 2
 3import org.springframework.stereotype.Service;
 4
 5import com.its.reservation.repository.Reservation;
 6
 7@Service
 8public class ReservationService {
 9	public Reservation getReservationDetails(String name) {
10		return null;
11	}
12}

We also update ReservationController by wiring required dependency

 1@RestController
 2@RequestMapping("/reservation")
 3public class ReservationController {
 4	
 5	private ReservationService reservationService;
 6	
 7	public ReservationController(ReservationService reservationService) {
 8		this.reservationService = reservationService;
 9	}
10
11	@RequestMapping(method = RequestMethod.GET, value = "/{name}")
12	private Reservation getReservation(@PathVariable String name) {
13		return reservationService.getReservationDetails(name);
14	}
15
16}

However, as per the rule of Mockist approach - dependencies (within application) for a Class Under Test should be mocked whilst performing Unit Testing. So we need to update ReservationControllerTest :

 1@RunWith(SpringRunner.class)
 2@WebMvcTest(ReservationController.class)
 3public class ReservationControllerTest {
 4	@Autowired
 5	private MockMvc mockMvc;
 6	
 7	@MockBean
 8	ReservationService reservationService;
 9	
10	@Test
11	public void getReservation_shouldReturnReservationInfo() {
12		BDDMockito.given(reservationService.getReservationDetails(ArgumentMatchers.anyString()))
13					.willReturn(new Reservation(Long.valueOf(1), "Dhaval", "Shah"));
14		try {
15			mockMvc.perform(MockMvcRequestBuilders.get("/reservation/Dhaval"))
16				.andExpect(MockMvcResultMatchers.status().isOk())
17				.andExpect(MockMvcResultMatchers.jsonPath("firstName").value("Dhaval"))
18				.andExpect(MockMvcResultMatchers.jsonPath("lastName").value("Shah"));
19		} catch (Exception e) {
20			e.printStackTrace();
21		}
22	}
23}

When we run above test . . . . . It passes - which means that Controller implementation is as per its expected behavior. So now we have some kind of safety net for our ReservationController . . Vola !

However, one might still feel that test driving happy path flows is relatively easier than business exceptions being thrown during workflow execution. So we will write a test case for a scenario which might end up in a scenario where in user is not available in our system i.e. ReservationNotFoundException. Also our unit test case will have to mock this exception

 1@RunWith(SpringRunner.class)
 2@WebMvcTest(ReservationController.class)
 3public class ReservationControllerTest {
 4	@Autowired
 5	private MockMvc mockMvc;
 6	
 7	@MockBean
 8	ReservationService reservationService;
 9	
10	@Test
11	public void getReservation_shouldReturnReservationInfo() {
12		BDDMockito.given(reservationService.getReservationDetails(ArgumentMatchers.anyString()))
13					.willReturn(new Reservation(Long.valueOf(1), "Dhaval", "Shah"));
14		try {
15			mockMvc.perform(MockMvcRequestBuilders.get("/reservation/Dhaval"))
16				.andExpect(MockMvcResultMatchers.status().isOk())
17				.andExpect(MockMvcResultMatchers.jsonPath("firstName").value("Dhaval"))
18				.andExpect(MockMvcResultMatchers.jsonPath("lastName").value("Shah"));
19		} catch (Exception e) {
20			e.printStackTrace();
21		}
22	}
23	
24	@Test
25	public void getReservation_NotFound() throws Exception {
26		BDDMockito.given(reservationService.getReservationDetails(ArgumentMatchers.anyString()))
27					.willThrow(new ReservationNotFoundException());
28		
29		mockMvc.perform(MockMvcRequestBuilders.get("/reservation/Dhaval"))
30			.andExpect(MockMvcResultMatchers.status().isNotFound());
31	}
32}

In order to pass the newly added above test case, ReservationController also needs to be updated by having required behavior for handling exceptions

 1@RestController
 2@RequestMapping("/reservation")
 3public class ReservationController {
 4	private ReservationService reservationService;
 5	
 6	public ReservationController(ReservationService reservationService) {
 7		this.reservationService = reservationService;
 8	}
 9
10	@RequestMapping(method = RequestMethod.GET, value = "/{name}")
11	private Reservation getReservation(@PathVariable String name) {
12		System.out.println("Entering and leaving ReservationController : getReservation after fetching service");
13		return reservationService.getReservationDetails(name);
14	}
15	
16	@ExceptionHandler()
17	@ResponseStatus(HttpStatus.NOT_FOUND)
18	public void userNotFoundHandler(ReservationNotFoundException rnfe) {
19		System.out.println("Entering and leaving ReservationController : userNotFoundHandler");
20	}
21}

Within the purview of our feature, this completes the unit testing of REST endpoint i.e Controller. Now we move to business layer i.e. ReservationService which has been discovered as a collaborator of ReservationController.

Business / Service Layer

Since ReservationService  is a plain Spring bean, we just need to write a plain unit test which is devoid of any Spring dependency - and of course it will fail as our class under test i.e.ReservationService is returning null.

 1package com.its.reservation;
 2
 3import static org.assertj.core.api.Assertions.assertThat;
 4
 5import org.junit.Test;
 6import org.springframework.beans.factory.annotation.Autowired;
 7
 8import com.its.reservation.repository.Reservation;
 9import com.its.reservation.service.ReservationService;
10
11@RunWith(MockitoJUnitRunner.class)
12public class ReservationServiceTest {
13	ReservationService reservationservice;
14	
15	@Before
16	public void setUp() throws Exception {
17		reservationService = new ReservationService();
18	}
19	
20	@Test
21	public void getReservationDetails_returnsReservationInfo() {
22		Reservation aReservation = reservationservice.getReservationDetails("Dhaval");
23		
24		assertThat(aReservation.getFirstName()).isEqualTo("Dhaval");
25		assertThat(aReservation.getFirstName()).isEqualTo("Shah");
26	}
27}

In real world application, business layer i.e. Service class will just be an orchestrator of business workflow, which needs to be executed for returning back required response to Controller. For the sake of simplicity our service layer will just be responsible for fetching required information from database. Hence it will need ReservationRepository as a collaborator, which will be able to fetch required data from database. So we will be creating corresponding Repository which just ensures that my class under test i.e. ReservationService is free from any compilation errors

1package com.its.reservation.repository;
2
3import org.springframework.data.repository.CrudRepository;
4
5public interface ReservationRepository extends CrudRepository<Reservation, Long> {
6	Reservation findByFirstName(String name);
7}

So with the introduction of new above collaborator, ReservationService would look like

 1package com.its.reservation.service;
 2
 3import org.springframework.stereotype.Service;
 4
 5import com.its.reservation.repository.Reservation;
 6import com.its.reservation.repository.ReservationRepository;
 7
 8@Service
 9public class ReservationService {
10	private ReservationRepository reservationRepository;
11	
12	public ReservationService(ReservationRepository reservationRepository) {
13		this.reservationRepository = reservationRepository;
14	}
15
16	public Reservation getReservationDetails(String name) {
17		return reservationRepository.findByFirstName(name);
18	}
19}

Now coming back to ReservationServiceTest which is now free of compilation errors; we will mock collaborator of ReservationService i.e. ReservationRepository

 1@RunWith(MockitoJUnitRunner.class)
 2public class ReservationServiceTest {
 3	ReservationService reservationService;
 4	
 5	@Mock
 6	ReservationRepository reservationRepository;
 7	
 8	@Before
 9	public void setUp() throws Exception {
10		reservationService = new ReservationService(reservationRepository);
11	}
12	
13	@Test
14	public void getReservationDetails_returnsReservationInfo() {
15		BDDMockito.given(reservationRepository.findByFirstName("Dhaval"))
16					.willReturn(new Reservation(Long.valueOf(1), "Dhaval", "Shah"));
17		
18		Reservation aReservation = reservationService.getReservationDetails("Dhaval");
19		
20		assertThat(aReservation.getFirstName()).isEqualTo("Dhaval");
21		assertThat(aReservation.getLastName()).isEqualTo("Shah");
22	}
23}

Lets also verify exceptional flows within this Service class. So we implement test case for the scenario where in ReservationRepository is returning null

 1@RunWith(MockitoJUnitRunner.class)
 2public class ReservationServiceTest {
 3	ReservationService reservationService;
 4	
 5	@Mock
 6	ReservationRepository reservationRepository;
 7	
 8	@Before
 9	public void setUp() throws Exception {
10		reservationService = new ReservationService(reservationRepository);
11	}
12	
13	@Test
14	public void getReservationDetails_returnsReservationInfo() {
15		BDDMockito.given(reservationRepository.findByFirstName("Dhaval"))
16					.willReturn(new Reservation(Long.valueOf(1), "Dhaval", "Shah"));
17		
18		Reservation aReservation = reservationService.getReservationDetails("Dhaval");
19		
20		assertThat(aReservation.getFirstName()).isEqualTo("Dhaval");
21		assertThat(aReservation.getLastName()).isEqualTo("Shah");
22	}
23	
24	@Test(expected = ReservationNotFoundException.class)
25	public void getReservationDetails_whenNotFound() {
26		BDDMockito.given(reservationRepository.findByFirstName("Dhaval")).willReturn(null);
27		Reservation aReservation = reservationService.getReservationDetails("Dhaval");
28	}
29}

Of course this test case will fail as our current implementation of *ReservationService is not having required logic for handling no results returned by ReservationRepository. Hence we update our ReservationService

 1@Service
 2public class ReservationService {
 3	private ReservationRepository reservationRepository;
 4	
 5	public ReservationService(ReservationRepository reservationRepository) {
 6		this.reservationRepository = reservationRepository;
 7	}
 8
 9	public Reservation getReservationDetails(String name) {
10		System.out.println("Entering and leaving ReservationService : getReservationDetails "
11				+ "after calling reservationRepository.findByFirstName");
12		Reservation aReservation = reservationRepository.findByFirstName(name);
13		if (aReservation == null) {
14			throw new ReservationNotFoundException();
15		}
16		return aReservation;
17	}
18}

Within the purview of our feature, this completes the unit testing of ReservationService.

Repository

Now we move to the respository layer i.e. ReservationRepository which has been discovered as a collaborator of ReservationService.

Even though from implementation standpoint it might seem trivial to test - but its other way round. Reason being, lot of complexity is camouflaged behind CrudRepository which needs to be considered whilst implementing test case for a Repository. In addition we will also need a mechanism to generate test data without database - Many thanks to H2 Database which can be used for our development purpose and also to Spring Boot starter which helps in getting required dependencies of H2 Database. Just to reiterate, we will start with a failing test and then add required implementation to pass this test.

 1package com.its.reservation;
 2
 3import org.assertj.core.api.Assertions;
 4import org.junit.Test;
 5import org.junit.runner.RunWith;
 6import org.springframework.beans.factory.annotation.Autowired;
 7import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
 8import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
 9import org.springframework.test.context.junit4.SpringRunner;
10
11import com.its.reservation.repository.Reservation;
12import com.its.reservation.repository.ReservationRepository;
13
14/**
15 * @author Dhaval
16 *
17 */
18@RunWith(SpringRunner.class)
19@DataJpaTest
20public class ReservationRepositoryTest {
21	@Autowired
22	TestEntityManager entityManager;
23	
24	@Autowired
25	ReservationRepository reservationRepository;
26	
27	@Test
28	public void getReservation_returnReservationDetails() {
29		Reservation savedReservation = entityManager.persistAndFlush(new Reservation("Dhaval","Shah"));
30		
31		Reservation aReservation = reservationRepository.findByFirstName("Dhaval");
32		
33		Assertions.assertThat(aReservation.getFirstName()).isEqualTo(savedReservation.getFirstName());
34		Assertions.assertThat(aReservation.getLastName()).isEqualTo(savedReservation.getLastName());
35	}
36}

@DataJpaTest - Spring annotation that can be used for a JPA test. This will ensure that AutoConfiguration are disabled and JPA specific configurations are applied. By default it will use H2 database which can be changed by using @AutoConfigureTestDatabase and application.properties. One can change database according to environment by using Spring profile

TestEntityManager - Along with few methods of EntityManager it provides helper methods for testing JPA implementation. We can customize TestEntityManager by using @AutoConfigureTestEntityManager

End to End Test - Final Version

We are still remaining to get our ReservationEndToEndTest pass :) So we will update this test such that H2 database is populated with required data which can be used for asserting post invocation of API endpoint. And by this we are able to complete our feature implementation with Outside - In / Mockist style of TDD

 1@RunWith(SpringRunner.class)
 2@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
 3public class ReservationEndToEndTest {
 4	@Autowired
 5	TestRestTemplate testRestTemplate;
 6	
 7	@Test
 8	public void getReservation_shouldReturnReservationDetails() {
 9		// Arrange
10		
11		// Act
12		ResponseEntity<Reservation> response = testRestTemplate.getForEntity("/reservation/{name}", 
13		
14		// Assert
15		Assertions.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
16		Assertions.assertThat(response.getBody().getFirstName()).isEqualTo("Dhaval");
17		Assertions.assertThat(response.getBody().getLastName()).isEqualTo("Shah");
18		Assertions.assertThat(response.getBody().getId()).isNotNull();
19	}
20}
21
22@Component
23class SampleDataCLR implements CommandLineRunner {
24	private ReservationRepository reservationRepository;
25	
26	public SampleDataCLR(ReservationRepository reservationRepository) {
27		this.reservationRepository = reservationRepository;
28	}
29
30	@Override
31	public void run(String... args) throws Exception {
32		System.out.println("@@@@@@@@@@@@@@ Entering SampleDataCLR : run");
33		reservationRepository.save(new Reservation("Dhaval","Shah"));
34		reservationRepository.save(new Reservation("Yatharth","Shah"));
35		reservationRepository.findAll().forEach(System.out :: println);
36		System.out.println("@@@@@@@@@@@@@@ Leaving SampleDataCLR : run");
37	}
38}

Testing Non Functional Requirement - Caching

In order to test caching, first we will need to enable caching within our Spring Boot application via @EnableCaching i.e.

 1import org.springframework.boot.SpringApplication;
 2import org.springframework.boot.autoconfigure.SpringBootApplication;
 3import org.springframework.cache.annotation.EnableCaching;
 4
 5@SpringBootApplication
 6@EnableCaching
 7public class BootifulTddApplication {
 8
 9	public static void main(String[] args) {
10		SpringApplication.run(BootifulTddApplication.class, args);
11	}
12}

@EnableCaching - Responsible for registering necessary Spring components based on @Cacheable annotation

Next thing that needs to be done is - we annotate our business API i.e getReservationDetails() within ReservationService with @Cacheable

 1@Service
 2public class ReservationService {
 3	
 4	private ReservationRepository reservationRepository;
 5	
 6	public ReservationService(ReservationRepository reservationRepository) {
 7		this.reservationRepository = reservationRepository;
 8	}
 9
10	@Cacheable("reservation")
11	public Reservation getReservationDetails(String name) {
12		System.out.println("Entering and leaving ReservationService : getReservationDetails "
13				+ "after calling reservationRepository.findByFirstName");
14		Reservation aReservation = reservationRepository.findByFirstName(name);
15		if (aReservation == null) {
16			throw new ReservationNotFoundException();
17		}
18		return aReservation;
19	}
20
21}

With this NFR the key question is, how can we test Caching implementation without any actual cache? We can still test this implementation with ReservationCachingTest as shown below

 1package com.its.reservation;
 2
 3import org.junit.Test;
 4import org.junit.runner.RunWith;
 5import org.mockito.ArgumentMatchers;
 6import org.mockito.BDDMockito;
 7import org.mockito.Mockito;
 8import org.springframework.beans.factory.annotation.Autowired;
 9import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
10import org.springframework.boot.test.context.SpringBootTest;
11import org.springframework.boot.test.mock.mockito.MockBean;
12import org.springframework.test.context.junit4.SpringRunner;
13
14import com.its.reservation.repository.Reservation;
15import com.its.reservation.repository.ReservationRepository;
16import com.its.reservation.service.ReservationService;
17
18@RunWith(SpringRunner.class)
19@SpringBootTest(webEnvironment=SpringBootTest.WebEnvironment.NONE)
20@AutoConfigureTestDatabase
21public class ReservationCachingTest {
22	@Autowired
23	ReservationService reservationService;
24	
25	@MockBean
26	ReservationRepository reservationRepository;
27	
28	@Test
29	public void caching_reducesDBCall() {
30		BDDMockito.given(reservationRepository.findByFirstName(ArgumentMatchers.anyString()))
31			  .willReturn(new Reservation(Long.valueOf(1),"Dhaval","Shah"));
32		
33		reservationService.getReservationDetails("Dhaval");
34		reservationService.getReservationDetails("Dhaval");
35		
36		Mockito.verify(reservationRepository, Mockito.times(1)).findByFirstName("Dhaval");
37	}
38}

Finally we have covered all aspects of the feature that we were suppose to implement. I do know it has got bit too long, but the nature of topic and relevance it has with chronological order of evolution of implementation does not allow me to convert it into two parts !

Conclusion

As we saw throughout the implementation, Spring Boot has lot of features to easily test Spring Boot application. We can use them as per our need to ensure that we are not only able to have required safety nets whilst implementing them but can also verify / validate design decisions.

Testing is an indispensable practice of software development. I hope this article will be of some help to get you started with Test Driven Development of Spring Boot applications.

Source Code : Bootiful TDD

comments powered by Disqus