Introduction
Testing can be quite hard especially when you’re not sure where to start. Normally things are so coupled it’s hard to isolate components for testing. In Spring we can test just our business logic by mocking dependencies when are code is messy and coupled. We need to also consider keeping unit tests separate from integration tests. Ideally unit tests should be fast and should not depend on external systems as what we wish to test is our business logic in isolation. So normally we will use mocks of our repositories and services.
In this article my goal is to show you how to write basic unit and integration tests in Spring Boot using JUnit and Mockito. For our integration tests we will use Testcontainers and Restassured. Testconainers is used to test with equivalent databases as the ones you’d use in production. Hence, giving you more confidence.
You can find the code for this article in this repo
Unit tests
The goal of your unit tests is to validate your business logic and so you need not concern yourself with external dependencies such databases. As such these external dependencies should be mocked. The service tests below demonstrate this using Mockito.
@ExtendWith(MockitoExtension.class)
public class OrderServiceTests
{
// will create an instance of OrderService and inject the mocks into it
@InjectMocks
private OrderService orderService;
// Will be injected into orderService
@Mock
private ProductRepository productRepository;
@Test
public void givenProductOutOfStock_whenMakeOrderBySku_thenThrowIllegalStateException() {
// Arrange
String sku = "SKU1001";
Mockito.when(productRepository.findBySku(sku)).thenReturn(Optional.of(new Product(1L,"Charger", BigDecimal.ZERO , 0, sku)));
// Act && Assert
assertThrows(IllegalStateException.class, () -> {
orderService.makeOrderBySku(sku, 1);
});
}
@Test
public void givenInsufficientProductStock_whenMakeOrderBySku_thenThrowIllegalStateException() {
// Arrange
String sku = "SKU1002";
Mockito.when(productRepository.findBySku(sku)).thenReturn(Optional.of(new Product(1L,"Headphones", BigDecimal.valueOf(3) , 2, sku)));
// Act && Assert
assertThrows(IllegalStateException.class, () -> {
orderService.makeOrderBySku(sku, 5);
});
}
}
Notice this line:
Mockito.when(productRepository.findBySku(sku)).thenReturn(Optional.of(new Product(1L,"Charger", BigDecimal.ZERO , 0, sku)));
This is because we want to not access the repository which actually would normally hit the DB to retrieve the product. Instead, we return a new dummy product. This allows our service to behave as if it were retrieving a product from the database without actually doing so.
We then make an assertion for the expected exception when we try to make an order for a product that is out of stock.
assertThrows(IllegalStateException.class, () -> {
orderService.makeOrderBySku(sku, 1);
});
Do note that there’s a plethora of assertions you can make. You can check for equality, nullability, and many more.
The important thing here is to isolate your business logic from external dependencies by mocking them.
Integration tests
In Spring Boot when I think of integration tests I normally think of the controller as it’s the entry point into our application. The controller will end up orchestrating calls to services. This makes it a good candidate to test integration tests, but this could also be cron jobs or any point of entry into your application. So you want to test integration entry points to ensure that all the components work together as expected.
Without further ado, let me show you how I would write my rest controller integration tests using Testcontainers and Restassured.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class OrderControllerIT {
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15").waitingFor(Wait.defaultWaitStrategy());
@LocalServerPort
private Integer port;
@BeforeEach
void setUp() {
RestAssured.baseURI = "http://localhost:" + port;
}
@Test
public void makeOrder_ShouldCreateOrderSuccessfully() throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
OrderRequest orderRequest = new OrderRequest("SKU001", 2, null);
given()
.contentType(ContentType.JSON)
.body(mapper.writeValueAsString(orderRequest))
.when()
.post("/orders")
.then()
.statusCode(200)
.body("id", notNullValue())
.body("productSku", equalTo("SKU001"))
.body("qty", equalTo(2));
}
@Test
public void makeOrder_thenGetCreatedOrderById_ShouldReturnOrder() throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
OrderRequest orderRequest = new OrderRequest("SKU002", 3, null);
// Create Order
Integer orderId =
given()
.contentType(ContentType.JSON)
.body(mapper.writeValueAsString(orderRequest))
.when()
.post("/orders")
.then()
.statusCode(200)
.extract()
.path("id");
// Retrieve Order by ID
given()
.when()
.get("/orders/{id}", orderId)
.then()
.statusCode(200)
.body("id", equalTo(orderId));
}
}
So here we test our endpoints. We want to make sure that everything works well together when we create an order. We also want to ensure that once an order is created that we get back the same order when we retrieve it by id.
Summary
- Unit tests should be small and fast and focus on testing business logic in isolation by mocking external dependencies.
- Integration tests should test entry points into your application such as controllers to ensure that all components work
- Use Testcontainers to test with real databases in your integration tests, this should give you better confidence that your application will work in production.
- Use Restassured to test your REST endpoints in integration tests.