4.9.2. Интеграционные тесты веб-уровня

Интеграционные тесты веб-уровня выполняются в контейнере Spring блока Web Client. Тестовый контейнер работает независимо от среднего слоя, так как фреймворк автоматически создает заглушки (stubs) для всех сервисов. Тестовая инфраструктура состоит из следующих классов, расположенных в пакете com.haulmont.cuba.web.testsupport и его вложенных пакетах:

  • TestContainer - обертка вокруг контейнера Spring для использования в качестве базового класса в проектах.

  • TestServiceProxy - предоставляет заглушки по умолчанию для сервисов среднего слоя. Данный класс можно также использовать для регистрации моков сервисов, специфичных для теста: см. статический метод mock().

  • DataServiceProxy - заглушка по умолчанию для DataManager. Содержит реализацию метода commit(), которая имитирует поведение реального хранилища: делает новые экземпляры сущностей detached, инкрементирует версии, и т.д. Методы загрузки ничего не делают и возвращают null и пустые коллекции.

  • TestUiEnvironment - предоставляет набор методов для конфигурирования и получения TestContainer. Экземпляр данного класса должен быть использован в тестах в качестве JUnit 5 extension.

  • TestEntityFactory - фабрика для удобного создания тестовых экземпляров сущностей. Может быть получена из TestContainer.

Несмотря на то, что фреймворк предоставляет дефолтные заглушки для сервисов среднего слоя, в тестах могут понадобится собственные моки сервисов. Для создания моков можно использовать любой мок-фреймворк, просто подключив его в зависимости проекта как описано в разделе выше. Моки сервисов регистрируются с помощью метода TestServiceProxy.mock().

Пример контейнера для интеграционных тестов веб-уровня

Создайте каталог test в модуле web. Затем создайте в подходящем пакете данного каталога класс контейнера:

package com.company.demo;

import com.haulmont.cuba.web.testsupport.TestContainer;

import java.util.Arrays;

public class DemoWebTestContainer extends TestContainer {

    public DemoWebTestContainer() {
        appComponents = Arrays.asList(
                "com.haulmont.cuba"
                // add CUBA add-ons and custom app components here
        );
        appPropertiesFiles = Arrays.asList(
                // List the files defined in your web.xml
                // in appPropertiesConfig context parameter of the web module
                "com/company/demo/web-app.properties",
                // Add this file which is located in CUBA and defines some properties
                // specifically for test environment. You can replace it with your own
                // or add another one in the end.
                "com/haulmont/cuba/web/testsupport/test-web-app.properties"
        );
    }

    public static class Common extends DemoWebTestContainer {

        // A common singleton instance of the test container which is initialized once for all tests
        public static final DemoWebTestContainer.Common INSTANCE = new DemoWebTestContainer.Common();

        private static volatile boolean initialized;

        private Common() {
        }

        @Override
        public void before() throws Throwable {
            if (!initialized) {
                super.before();
                initialized = true;
            }
            setupContext();
        }

        @Override
        public void after() {
            cleanupContext();
            // never stops - do not call super
        }
    }
}
Пример теста экрана UI

Ниже приведен пример теста, который проверяет состояние редактируемой в экране сущности после некоторого действия пользователя.

package com.company.demo.customer;

import com.company.demo.DemoWebTestContainer;
import com.company.demo.entity.Customer;
import com.company.demo.web.screens.customer.CustomerEdit;
import com.haulmont.cuba.gui.Screens;
import com.haulmont.cuba.gui.components.Button;
import com.haulmont.cuba.gui.screen.OpenMode;
import com.haulmont.cuba.web.app.main.MainScreen;
import com.haulmont.cuba.web.testsupport.TestEntityFactory;
import com.haulmont.cuba.web.testsupport.TestEntityState;
import com.haulmont.cuba.web.testsupport.TestUiEnvironment;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import java.util.Collections;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;

public class CustomerEditInteractionTest {

    @RegisterExtension
    TestUiEnvironment environment =
            new TestUiEnvironment(DemoWebTestContainer.Common.INSTANCE).withUserLogin("admin"); (1)

    private Customer customer;

    @BeforeEach
    public void setUp() throws Exception {
        TestEntityFactory<Customer> customersFactory =
                environment.getContainer().getEntityFactory(Customer.class, TestEntityState.NEW);

        customer = customersFactory.create(Collections.emptyMap()); (2)
    }

    @Test
    public void testGenerateName() {
        Screens screens = environment.getScreens(); (3)

        screens.create(MainScreen.class, OpenMode.ROOT).show(); (4)

        CustomerEdit customerEdit = screens.create(CustomerEdit.class); (5)
        customerEdit.setEntityToEdit(customer);
        customerEdit.show();

        assertNull(customerEdit.getEditedEntity().getName());

        Button generateBtn = (Button) customerEdit.getWindow().getComponent("generateBtn"); (6)
        customerEdit.onGenerateBtnClick(new Button.ClickEvent(generateBtn)); (7)

        assertEquals("Generated name", customerEdit.getEditedEntity().getName());
    }
}
1 - создание экземпляра TestUiEnvironment с разделяемым контейнером и пользователем admin в заглушке пользовательской сессии.
2 - создание экземпляра сущности в состоянии new.
3 - получение инфраструктурного объекта Screens.
4 - открытие главного экрана, который необходим для открытия остальных экранов приложения.
5 - создание, инициализация и открытие экрана редактирования сущности.
6 - получение компонента Button.
7 - создание объекта события нажатия кнопки и вызов метода контроллера, который реагирует на это событие.
Пример теста загрузки данных в экран

Ниже приведен пример теста, который проверяет корректность загрузки данных в экране UI.

package com.company.demo.customer;

import com.company.demo.DemoWebTestContainer;
import com.company.demo.entity.Customer;
import com.company.demo.web.screens.customer.CustomerEdit;
import com.haulmont.cuba.core.app.DataService;
import com.haulmont.cuba.core.entity.Entity;
import com.haulmont.cuba.core.global.LoadContext;
import com.haulmont.cuba.gui.Screens;
import com.haulmont.cuba.gui.model.InstanceContainer;
import com.haulmont.cuba.gui.screen.OpenMode;
import com.haulmont.cuba.gui.screen.UiControllerUtils;
import com.haulmont.cuba.web.app.main.MainScreen;
import com.haulmont.cuba.web.testsupport.TestEntityFactory;
import com.haulmont.cuba.web.testsupport.TestEntityState;
import com.haulmont.cuba.web.testsupport.TestUiEnvironment;
import com.haulmont.cuba.web.testsupport.proxy.TestServiceProxy;
import mockit.Delegate;
import mockit.Expectations;
import mockit.Mocked;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class CustomerEditLoadDataTest {

    @RegisterExtension
    TestUiEnvironment environment =
            new TestUiEnvironment(DemoWebTestContainer.Common.INSTANCE).withUserLogin("admin"); (1)

    @Mocked
    private DataService dataService; (1)

    private Customer customer;

    @BeforeEach
    public void setUp() throws Exception {
        new Expectations() {{ (2)
            dataService.load((LoadContext<? extends Entity>) any);
            result = new Delegate() {
                Entity load(LoadContext lc) {
                    if ("demo_Customer".equals(lc.getEntityMetaClass())) {
                        return customer;
                    } else
                        return null;
                }
            };
        }};

        TestServiceProxy.mock(DataService.class, dataService); (3)

        TestEntityFactory<Customer> customersFactory =
                environment.getContainer().getEntityFactory(Customer.class, TestEntityState.DETACHED);

        customer = customersFactory.create(
                "name", "Homer", "email", "homer@simpson.com"); (4)
    }

    @AfterEach
    public void tearDown() throws Exception {
        TestServiceProxy.clear(); (5)
    }

    @Test
    public void testLoadData() {
        Screens screens = environment.getScreens();

        screens.create(MainScreen.class, OpenMode.ROOT).show();

        CustomerEdit customerEdit = screens.create(CustomerEdit.class);
        customerEdit.setEntityToEdit(customer);
        customerEdit.show();

        InstanceContainer customerDc = UiControllerUtils.getScreenData(customerEdit).getContainer("customerDc"); (6)
        assertEquals(customer, customerDc.getItem());
    }
}
1 - определение мока с помощью фреймворка JMockit.
2 - задание поведения мока.
3 - регистрация мока.
4 - создание экземпляра сущности в состоянии detached.
5 - удаление мока после завершения теста.
6 - получение контейнера данных.