Выполнение одиночного теста из Suite с @ClassRule завершается ошибкой

Чтобы создать среду только один раз и избежать наследования, я определил класс JUnit Suite с помощью @ClassRule:

@RunWith(Suite.class)               
@Suite.SuiteClasses({               
  SuiteTest1.class              
})      

public class JUnitTest {

    @ClassRule
    private static DockerComposeContainer env = ...


    @BeforeClass
    public static void init(){
        ...
    }

    ...

}

И есть класс Test, который использует env в методе тестирования:

public class SuiteTest1 {               

    @Test
    public void method(){
        client.query(...);// Executes a query against docker container


    }
}

Когда я выполняю тесты с помощью Test Suite, все работает, как ожидалось. Но когда я напрямую пытаюсь запустить (даже с IDE) тестовый класс SuiteTest1, он терпит неудачу, и ничего из пакета не вызывается (то есть @ClassRule и @BeforeClass).

Есть ли какие-либо предложения о том, как добиться также однократного выполнения SuiteTest1 хорошим способом (без вызова статических методов JUnitTest из SuiteTest1)?

У вас заблуждение. В вашем случае пакет будет управлять запуском и остановкой контейнера докеров. Однако он не будет выполнять какие-либо инъекции зависимостей переменной env в ваши тестовые классы. Кроме того, любые поля, помеченные @Rule или @ClassRule, должны быть общедоступными, который также работает только с подтипами TestRule, где я не уверен, является ли DockerContainer одним из них. Поскольку env уже статичен, вы можете получить к нему доступ в своих тестовых классах через JUnitTest.env после изменения его на общедоступный, что вам все равно нужно сделать.

Roman Vottner 28.09.2018 13:33

Привет, @RomanVottner, спасибо за ответ. Пакет запускает и останавливает контейнер, если я запускаю его. Мой вопрос касается запуска одного теста пакета (например, из IDE): кажется, что если я запускаю только тестовый класс, контейнеры докеров не запускаются. Знаете ли вы, как решить обе проблемы: 1 - Запустить один раз докер-контейнеры для каждого набора 2 - Запустить один раз докер-контейнеры, если я запускаю только один тест

Momo 28.09.2018 14:20

Тестовый класс не наследует вещи, определенные в наборе тестов. Кроме того, он вообще не знает о том, что является участником определенного (или нескольких) наборов тестов. По сути, вам нужно создать набор тестов для каждой комбинации, которую вы, возможно, захотите затем запустить, или переместить материал из набора в соответствующий тестовый класс.

Roman Vottner 28.09.2018 14:42

Хорошо, но для сценария использования интеграционного теста это означало бы запуск / отключение среды докеров для каждого тестового класса ... что в основном убивает производительность фазы тестирования. Цель состоит в том, чтобы развернуть env один раз для всех тестовых классов, возможно, мне придется поднять сценарий до testcontainers

Momo 28.09.2018 14:48

Либо скопируйте свой набор тестов и добавьте только те тестовые классы, которые вы хотите запустить в своей среде IDE, либо временно измените существующий набор тестов и закомментируйте классы, которые вы не хотите запускать в своей среде IDE. Также можно попробовать запустить свои тесты более программно

Roman Vottner 28.09.2018 14:54

Еще один способ - написать тестовое правило, которое в основном выполняет работу по запуску и остановке контейнера докеров. Вам нужно сделать это внутри синглтона, то есть сначала проверить, создан ли уже экземпляр заранее. Это позволит вам определить правило тестирования как в наборе, так и в тестовом классе. При выполнении набора тестов набор тестов будет инициализировать контейнер, а последующие тестовые классы будут игнорировать задачу, в то время как выполнение одного тестового класса инициализирует контейнер, поскольку ни один набор не делал этого раньше.

Roman Vottner 28.09.2018 15:00

Да, я думал об этом, но решение мне не очень понравилось. Но спасибо!

Momo 28.09.2018 16:49
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
2
7
917
1

Ответы 1

Перефразируя вопрос: вам нужен набор JUnit с хуками до и после всех, которые также будут запускаться при запуске тестов один за другим (например, из IDE).

AFAIK JUnit 4 не предоставляет ничего нестандартного для этого, но если вы согласны с включением некоторых сторонних зависимостей Spring (весеннее испытание и весенний контекст) в ваш проект, я могу предложить обходной путь, который я использовал.

Пример полного кода того, что следует в этом посте можно найти здесь.

Решение (с использованием Spring)

Мы будем использовать контекст Spring для реализации нашей инициализации и очистки. Добавим базовый класс для наших тестов:

@ContextConfiguration(initializers = AbstractTestClass.ContextInitializer.class)
public class AbstractTestClass {
    
    @ClassRule
    public final static SpringClassRule springClassRule = new SpringClassRule();

    @Rule
    public final SpringMethodRule springMethodRule = new SpringMethodRule();

    public static class ContextInitializer
            implements ApplicationContextInitializer<ConfigurableApplicationContext> {
        @Override
        public void initialize(ConfigurableApplicationContext context) {
            System.out.println("Initializing context");

            context.addApplicationListener(
                    (ApplicationListener<ContextClosedEvent>)
                            contextClosedEvent ->
                                    System.out.println("Closing context"));
        }
    }
}

Обратите внимание на правила JUnit SpringClassRule и SpringMethodRule, которые расширяют наш базовый класс с помощью Spring-superpowers (обработка аннотаций Spring test - в данном случае ContextConfiguration, но там есть еще много плюсов - подробности см. В Справочник по весеннему тестированию). Вы можете использовать SpringRunner для этой цели, но это гораздо менее гибкое решение (поэтому опущено).

Тестовые классы:

public class TestClass1 extends AbstractTestClass {
    
    @Test
    public void test() {
        System.out.println("TestClass1 test");
    }
}

public class TestClass2 extends AbstractTestClass {
    
    @Test
    public void test() {
        System.out.println("TestClass2 test");
    }
}

И набор тестов:

@RunWith(Suite.class)
@SuiteClasses({TestClass1.class, TestClass2.class})
public class TestSuite {
}

Вывод при запуске пакета (для краткости удалены журналы, специфичные для Spring):

Initializing context
TestClass1 test
TestClass2 test
Closing context

Вывод при запуске одиночного теста (TestClass1):

Initializing context
TestClass1 test
Closing context

Слово объяснения

Это работает из-за кеширования контекста Spring. Цитата из документов:

Once the TestContext framework loads an ApplicationContext (or WebApplicationContext) for a test, that context is cached and reused for all subsequent tests that declare the same unique context configuration within the same test suite. To understand how caching works, it is important to understand what is meant by “unique” and “test suite.”

-- https://docs.spring.io/spring/docs/5.1.2.RELEASE/spring-framework-reference/testing.html#testcontext-ctx-management-caching

Помните, что вы получите другой контекст (и другую инициализацию), если вы переопределите конфигурацию контекста (например, добавите еще один инициализатор контекста с ContextConfiguration) для любого из классов в иерархии (TestClass1 или TestClass2 в нашем примере).

Использование beans для совместного использования экземпляров

Вы можете определить beans в своем контексте. Они будут использоваться во всех тестах в одном контексте. Это может быть полезно для совместного использования объекта в наборе тестов (контейнер Testcontainers в вашем случае, судя по тегам).

Добавим bean:

@ContextConfiguration(initializers = AbstractTestClass.ContextInitializer.class)
public class AbstractTestClass {

    @ClassRule
    public final static SpringClassRule springClassRule = new SpringClassRule();

    @Rule
    public final SpringMethodRule springMethodRule = new SpringMethodRule();

    public static class ContextInitializer
            implements ApplicationContextInitializer<ConfigurableApplicationContext> {
        @Override
        public void initialize(ConfigurableApplicationContext context) {
            ADockerContainer aDockerContainer = new ADockerContainer();
            aDockerContainer.start();

            context.getBeanFactory().registerResolvableDependency(
                    ADockerContainer.class, aDockerContainer);

            context.addApplicationListener(
                    (ApplicationListener<ContextClosedEvent>)
                            contextClosedEvent ->
                                    aDockerContainer.stop());
        }
    }
}

И внедрите его в тестовые классы:

public class TestClass1 extends AbstractTestClass {
    
    @Autowired
    private ADockerContainer aDockerContainer;

    @Test
    public void test() {
        System.out.println("TestClass1 test " + aDockerContainer.getData());
    }
}

public class TestClass2 extends AbstractTestClass {
    
    @Autowired
    private ADockerContainer aDockerContainer;

    @Test
    public void test() {
        System.out.println("TestClass2 test " + aDockerContainer.getData());
    }
}

ADockerContainer класс:

public class ADockerContainer {
    private UUID data;

    public void start() {
        System.out.println("Start container");
        data = UUID.randomUUID();
    }

    public void stop() {
        System.out.println("Stop container");
    }

    public String getData() {
        return data.toString();
    }
}

(Пример) вывод:

Start container
TestClass1 test 56ead80b-ec34-4dd6-9c0d-d6f07a4eb0d8
TestClass2 test 56ead80b-ec34-4dd6-9c0d-d6f07a4eb0d8
Stop container

Другие вопросы по теме