Integration Tests
An integration test is a broader type of test. It allows you to execute code in an environment close to the normal application runtime. When we talk about integration tests, we mean a test that starts the complete Spring context and interacts with a database if needed.
Use the New → Advanced → Integration Test action in the Jmix tool window to quickly create an integration test using Studio. |
The following example will check the OrderAmountCalculation
class again. But this time, we will test the class not as an isolated unit (as described in the previous section), but as part of the bigger context where it is used in the application. In this case, there is an EntityChangedEvent
listener for OrderLine
entities. As part of the persistence logic, the listener recalculates the order amount of the order, the order line belongs to, with the help of the OrderAmountCalculation
class:
@EventListener
public void recalculateOrderAmount(EntityChangedEvent<OrderLine> event) {
Order order = findOrderFromEvent(event);
BigDecimal amount = new OrderAmountCalculation().calculateTotalAmount(order.getLines());
order.setAmount(amount);
dataManager.save(order);
}
In the integration test, the OrderLineEventListener
and the OrderAmountCalculation
can be tested together. The test will create an order and an order line and store them in the database via the DataManager API. This will trigger the event listener, and the order amount will be calculated.
Dependency Injection in Tests
A Spring integration test can use the same dependency injection mechanism as the application code. In particular, you can use the @Autowired
annotation to inject beans into the test class. In this example, the DataManager
is injected into the test class to use it for transitively triggering the OrderLineEventListener
logic:
@SpringBootTest
public class OrderLineEventListenerTest {
@Autowired
DataManager dataManager;
// ...
}
If you need to test a custom bean directly, you can also directly inject the bean under test into the test class. In the following example, the CustomerService
is injected into the test class to directly execute its tested methods:
@SpringBootTest
public class CustomerServiceTest {
@Autowired
CustomerService customerService;
// ...
}
Database Interactions
There are two main reasons to interact with the database in an integration test.
The first one is that you might want to set up test data required for the execution of the test case. To interact with the database, you can use the regular Jmix features like DataManager
, as you would in production code.
The second reason is that the database can be accessed by the application logic executed in a test.
Let’s look at an example for both of those scenarios:
@Test
void given_customerWithEmailExists_when_findByEmail_then_customerFound() {
// given
Customer customer = dataManager.create(Customer.class);
customer.setEmail("customer@test.com");
dataManager.save(customer); (1)
// when
Optional<Customer> foundCustomer = customerService.findByEmail("customer@test.com"); (2)
// then
assertThat(foundCustomer)
.isPresent();
}
1 | The DataManager is used in the test to create a test customer in the database. |
2 | The CustomerService is used to perform a database lookup of customers by email. |
Test Data Cleanup
In the example above, the DataManager
stores a test customer in the database. Since all tests share the same database instance by default, it means that this test data will also be available for the next test. This is not a problem in this example, but in other cases, it might be. For example, consider that there is a unique constraint on the email address field of the Customer
entity. If you write a test that creates a customer with a specific email address, and another test that searches for a customer by email address - assuming it is not there, the second test would fail, because it would find the customer created by the first test.
There are several ways of cleaning up the test data. The first one is to keep references to entities that were created during the test. In the example above, you could keep a reference to the customer that was created in the test and delete it after the test has finished using dataManager.remove(customer)
. This is a valid approach, but it requires some additional code in the test. Additionally, it is not always possible to keep a reference to data that were created during the test. For example, if a new entity is created in the production code, you cannot get a reference to it in the test. Furthermore, in case of an exception during the test, the cleanup code might not be executed.
The second option would be to perform more general database cleanup. In the following example a JdbcTemplate
performs the SQL statement DELETE FROM CUSTOMER
to delete all customers from the database:
@Autowired (1)
DataSource dataSource;
@AfterEach (2)
void tearDown() {
JdbcTemplate jdbc = new JdbcTemplate(dataSource);
JdbcTestUtils.deleteFromTables(jdbc, "CUSTOMER"); (3)
}
1 | The DataSource is injected to instantiate the JdbcTemplate . |
2 | @AfterEach indicates to JUnit that this method should be executed after each test case. |
3 | The Spring JdbcTestUtils class provides a convenience method to delete all data from a database table. See more information about it in the Spring testing documentation. |
Security Context in Tests
Jmix allows code to be executed on behalf of a specified user, which is often necessary for testing functionalities that rely on user roles and permissions. This can be achieved by using the SystemAuthenticator.
Let’s consider an example of testing a CustomerService
method that behaves differently depending on the role of the user who executes it:
@Component
public class CustomerService {
@Autowired
private DataManager dataManager;
public Optional<Customer> findByEmail(String email) {
return dataManager.load(Customer.class)
.query("select c from sample_Customer c where c.email = :email")
.parameter("email", email)
.optional();
}
}
In this example, the CustomerService
has a method findCustomerByEmail
that returns a customer entity if found. The security policies enable access to customer data to only particular roles. This behavior can be tested by using the SystemAuthenticator
to execute the method as a particular user:
private final String USERNAME = "userWithoutPermissions";
@Test
void given_noPermissionsToReadCustomerData_when_findByEmail_then_nothingFound() {
// given
Customer customer = dataManager.create(Customer.class);
customer.setEmail("customer@test.com");
dataManager.save(customer);
// and
User userWithoutPermissions = dataManager.create(User.class);
userWithoutPermissions.setUsername(USERNAME);
dataManager.save(userWithoutPermissions); (1)
// when
Optional<Customer> foundCustomer = systemAuthenticator.withUser( (2)
USERNAME,
() -> customerService.findByEmail("customer@test.com") (3)
);
// then
assertThat(foundCustomer)
.isNotPresent();
}
1 | A new user without any role assignment is created for the test case. |
2 | SystemAuthenticator executes the code under test on behalf of the newly created user. |
3 | The CustomerService performs a database lookup of customers by email with the security context of that user. |
As the user has no roles, the customer service returns an empty Optional
.
AuthenticatedAsAdmin
Instead of setting up the security context for particular parts of the test, you can use the AuthenticatedAsAdmin
JUnit extension, which is automatically generated in a new Jmix project. It creates a security context before each test and sets the authenticated user to the admin user.
@SpringBootTest
@ExtendWith(AuthenticatedAsAdmin.class)
public class CustomerServiceTest {
// ...
It is also possible to combine the AuthenticatedAsAdmin
extension with the SystemAuthenticator
to execute the test code as a particular user. By annotating the test class the default security context is set to the admin. But within the test case, you can use the SystemAuthenticator
to execute the code as a particular user.
Overriding Application Behavior
Sometimes even for integration tests it is required to mock out certain parts of the application. In those cases, it is possible to combine the functionalities of @SpringBooTest
with Mockito and mock out particular beans, but still use the overall Spring context.
Let’s consider a NotificationService
that as part of its business logic uses the Emailer
API from the Email Sending add-on. An integration test for this service should not actually send out emails, so the email functionality should be mocked.
@MockBean
To mock a bean in a Spring integration test, you can use the @MockBean
annotation. In the following example, the Emailer
bean is mocked for the NotificationService
test described above:
package com.company.demo.app;
import com.company.demo.entity.Customer;
import io.jmix.email.EmailException;
import io.jmix.email.EmailInfo;
import io.jmix.email.Emailer;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doThrow;
@SpringBootTest
class NotificationServiceTest {
@MockBean
Emailer emailer;
@Autowired
NotificationService notificationService;
@Test
void given_emailDelivered_when_sendNotification_then_success() throws EmailException {
// given:
Customer customer = new Customer();
customer.setEmail("customer@company.com");
// when:
boolean success = notificationService.notifyCustomerAboutOrderDelivered(customer);
// then:
assertThat(success).isTrue();
}
@Test
void given_emailNotDelivered_when_sendNotification_then_noSuccess() throws EmailException {
// given:
doThrow(EmailException.class).when(emailer)
.sendEmail(any(EmailInfo.class));
// and:
Customer customer = new Customer();
customer.setEmail("customer@company.com");
// when:
boolean success = notificationService.notifyCustomerAboutOrderDelivered(customer);
// then:
assertThat(success).isFalse();
}
}
The @MockBean
annotation replaces the bean in the application context with a mock. This enables two things:
-
To prevent the email from actually being sent.
-
To simulate failure scenario where email sending did not work.
@TestConfiguration
In the example above, the @MockBean
annotation is used to replace the Emailer
bean with a mock. But this is not the only way to replace a bean in the application context. Another way is to use the @TestConfiguration
annotation. This annotation is set on a configuration class that is used only for the test. In the following example, the test configuration class replaces the Emailer
bean with a mock:
package com.company.demo.app;
import com.company.demo.entity.Customer;
import io.jmix.email.EmailException;
import io.jmix.email.EmailInfo;
import io.jmix.email.Emailer;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
@SpringBootTest
class NotificationServiceWithTestConfigurationTest {
@Autowired
NotificationService notificationService;
@TestConfiguration (1)
public static class EmailerTestConfiguration {
@Bean
public Emailer emailer() throws EmailException { (2)
Emailer emailer = mock(Emailer.class); (3)
doThrow(EmailException.class).when(emailer) (4)
.sendEmail(any(EmailInfo.class));
return emailer;
}
}
@Test
void given_emailNotDelivered_when_sendNotification_then_noSuccess() throws EmailException {
// given:
Customer customer = new Customer();
customer.setEmail("customer@company.com");
// when:
boolean success = notificationService.notifyCustomerAboutOrderDelivered(customer);
// then:
assertThat(success).isFalse();
}
}
1 | The inner static class annotated with @TestConfiguration will be picked up by Spring when executing the test case. |
2 | A bean with the name emailer of type Emailer is declared. It overrides the standard bean of this type. |
3 | A mock instance is created. |
4 | The behaviour of the mock is specified and the configured mock is returned. |
The production code interacting with the Emailer
bean will now use the mock instead of the standard implementation.