Testing Spring Boot applications

@RestController
public class ProductController {
@Autowired
private ProductService productService;

@GetMapping(value = "/status")
public String checkStatus(){
return "Live!";
}

@GetMapping("/products")
public Product getProduct(@RequestParam Long id){
boolean discount = true;
return productService.getProductById(id, discount);
}
}
public interface ProductService {
Product getProductById(Long id, boolean hasDiscount);
}
@Service
public class ProductServiceImpl implements ProductService {
@Autowired
private ProductRepository productRepository;

public Product getProductById(Long id, boolean hasDiscount) {
Product product = productRepository.getProductById(id);
if (product != null) {
if (hasDiscount) {
product.setPrice(product.getPrice() - 10);
}
return product;
} else {
throw new ProductNotFoundException();
}
}
}
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
Product getProductById(Long id);
}
@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String name;
private Double price;
private Long quantity;
private String storeName;
//constructors, getters and setters
}

Controller Unit tests

@ExtendWith(SpringExtension.class)
@WebMvcTest(ProductController.class)
class ProductControllerTest {

@Autowired
private MockMvc mockMvc;

@MockBean
private ProductService productServiceImpl; // This will mock a Spring Bean and Inject it where is needed

// This test uses assertEquals to check the validity of the response
@Test
void checkStatus_Should_ReturnLive_When_StatusPathIsCalled_AssertUsingAssertEquals() throws Exception {
//build request, execute GET to /status
MvcResult mvcResult = mockMvc
.perform(MockMvcRequestBuilders.get("/status").accept(MediaType.APPLICATION_JSON))
.andReturn();
assertEquals("Live!", mvcResult.getResponse().getContentAsString());
}

// This test uses ResultMatchers to check the validity of the response
@Test
void checkStatus_Should_ReturnLive_When_StatusPathIsCalled_AssertUsingResultMatchers() throws Exception {
//build request, execute GET to /status and assert result using Response Matchers
mockMvc.perform(MockMvcRequestBuilders.get("/status").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk()) //check is response status is 200
.andExpect(content().string("Live!"))
.andReturn();
}

// This test uses ResultMatchers with JSONAssert to check the validity of the response
@Test
void getProduct_Should_ReturnString_When_ProductsPathIsCalled_AssertUsingResultMatchers() throws Exception {
String expectedResult = "{\"id\":1,\"name\":\"Cheese\",\"price\":10.0,\"quantity\":100}";
String expectedResultWithoutSomePropertiesAndEscapeChars = "{id: 1, name: Cheese, price: 10.0}";

when(productServiceImpl.getProductById(1L, true))
.thenReturn(new Product(1L, "Cheese", 10.0, 100L));

// build request, execute GET to /product and assert result using Response Matchers with JSONAssert
mockMvc.perform(MockMvcRequestBuilders.get("/products?id=1").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk()) //check is response status is 200
.andExpect(content().json(expectedResult)) //it will succeed even if a property will be missing
.andExpect(content().json(expectedResultWithoutSomePropertiesAndEscapeChars))
.andReturn();
// behind the scene .andExpect(content().json(...)) uses JSONAssert calling assertEquals
// with strict mode deactivated

verify(productServiceImpl, times(1)).getProductById(1L, true);
}

// This test shows some of the capabilities of JSONAssert to check the validity of the response
@Test
void getProduct_Should_ReturnString_When_ProductsPathIsCalled_AssertUsingJSONAssert() throws Exception {
String expectedResult = "{\"id\":1,\"name\":\"Cheese\",\"price\":10.0,\"quantity\":100,\"storeName\":null}";
String expectedResultWithoutSomePropertiesAndEscapeChars = "{id:1,name:Cheese,price:10.0}";

when(productServiceImpl.getProductById(anyLong(), anyBoolean()))
.thenReturn(new Product(1L, "Cheese", 10.0, 100L));

// build request, execute GET to /products
MvcResult mvcResult = mockMvc
.perform(MockMvcRequestBuilders.get("/products?id=1").accept(MediaType.APPLICATION_JSON))
.andReturn();

// strict mode, everything should match, the structure should be the same
JSONAssert.assertEquals(expectedResult, mvcResult.getResponse().getContentAsString(), true);

// strict mode off, properties may be missing, escape characters can be missing too
JSONAssert.assertEquals(expectedResultWithoutSomePropertiesAndEscapeChars,
mvcResult.getResponse().getContentAsString(), false);

verify(productServiceImpl, times(1)).getProductById(anyLong(), anyBoolean());
}
}

Service Unit tests

@ExtendWith(MockitoExtension.class)
public class ProductSeviceTest {

@InjectMocks
private ProductService productService = new ProductServiceImpl();

@Mock
private ProductRepository productRepository;

@Test
void getProductById_Should_ReturnProduct_When_ParametersAreValidAndDiscountIsApplied(){
when(productRepository.getProductById(any())).thenReturn(new Product(1L, "Beer", 100.0, 100L));

Product product = productService.getProductById(1L, true);

assertEquals("Beer", product.getName());
assertEquals(90.0, product.getPrice());
verify(productRepository, times(1)).getProductById(any());
}

@Test
void getProductById_Should_ReturnProduct_When_ParametersAreValidAndDiscountIsNotApplied(){
when(productRepository.getProductById(any())).thenReturn(new Product(1L, "Beer", 100.0, 100L));

Product product = productService.getProductById(1L, false);

assertEquals("Beer", product.getName());
assertEquals(100.0, product.getPrice());
verify(productRepository, times(1)).getProductById(any());
}

@Test
void getProductById_Should_ThrowException_When_ProductIsNotFound(){
assertThrows(ProductNotFoundException.class, () -> {
when(productRepository.getProductById(any())).thenReturn(null);

Product product = productService.getProductById(1L, true);
});
}
}

Repository Unit tests

@ExtendWith(MockitoExtension.class)
public class ProductSeviceTest {

@InjectMocks
private ProductService productService = new ProductServiceImpl();

@Mock
private ProductRepository productRepository;

@Test
void getProductById_Should_ReturnProduct_When_ParametersAreValidAndDiscountIsApplied(){
when(productRepository.getProductById(any())).thenReturn(new Product(1L, "Beer", 100.0, 100L));

Product product = productService.getProductById(1L, true);

assertEquals("Beer", product.getName());
assertEquals(90.0, product.getPrice());
verify(productRepository, times(1)).getProductById(any());
}

@Test
void getProductById_Should_ReturnProduct_When_ParametersAreValidAndDiscountIsNotApplied(){
when(productRepository.getProductById(any())).thenReturn(new Product(1L, "Beer", 100.0, 100L));

Product product = productService.getProductById(1L, false);

assertEquals("Beer", product.getName());
assertEquals(100.0, product.getPrice());
verify(productRepository, times(1)).getProductById(any());
}

@Test
void getProductById_Should_ThrowException_When_ProductIsNotFound(){
assertThrows(ProductNotFoundException.class, () -> {
when(productRepository.getProductById(any())).thenReturn(null);

Product product = productService.getProductById(1L, true);
});
}
}

Integration tests

@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class ProductIntegrationTest {

@Autowired
private TestRestTemplate testRestTemplate;

@Autowired
private ProductRepository productRepository; // just to add data in db

@Test
void checkStatusApi_Should_ReturnString_When_Called() {
String response = testRestTemplate.getForObject("/status", String.class);
assertEquals(response, "Live!");
}

@Test
@DirtiesContext //@DirtiesContext will rollback database changes after test is done
void getProductApi_Should_ReturnProduct_When_Called() {
addSomeDataToDb();

ResponseEntity<Product> response = testRestTemplate
.getForEntity("/products?id=1", Product.class, new HashMap<String, String>());

assertEquals(HttpStatus.OK, response.getStatusCode());

assertEquals("Beer", response.getBody().getName());
assertEquals(10L, response.getBody().getQuantity());
assertEquals(100, response.getBody().getPrice());
}

private void addSomeDataToDb() {
Product p1 = new Product("Beer", 110.0, 10L);
Product p2 = new Product("Cheese", 100.0, 9L);
Product p3 = new Product("WIne", 110.0, 10L);
productRepository.save(p1);
productRepository.save(p2);
productRepository.save(p3);
}
}
@MockBean
private ProductRepository productRepositoryMock;
when(productRepositoryMock.method(..)).thenReturn(...)

Conclusion

--

--

--

Written by Gabriel Voicu — Senior Principal Lead Software Engineer @ Dell Technologies

Love podcasts or audiobooks? Learn on the go with our new app.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
WeInspire Technologies

WeInspire Technologies

Written by Gabriel Voicu — Senior Principal Lead Software Engineer @ Dell Technologies

More from Medium

Start with Spring Boot

Key Differences Between Spring Boot and Spring Framework

Simple CRUD with Spring Boot

Chain of Responsibility Pattern(Behavioral Design Pattern)