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:

OrderLineEventListener.java
@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:

OrderLineEventListenerTest.java
@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:

CustomerServiceTest.java
@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:

CustomerServiceTest.java
@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:

CustomerServiceTest.java
@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:

CustomerService.java
@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:

CustomerServiceTest.java
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.

CustomerServiceTest.java
@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:

  1. To prevent the email from actually being sent.

  2. 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.