Настройка состава JUnit5 тестов с помощью application.properties

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


    Теперь представьте, что не все тесты должны проходить в этих окружениях — кажому свой набор тестов.


    И предпочтительней настроить выбор, какие тесты должны выполняться, в… файле application.properties — кажому тесту свой переключатель "вкл/выкл".


    Звучит здорово, не правда ли?


    Тогда добро пожаловать под кат, где мы все это и реализуем с помощью SpringBoot 2 и JUnit 5.


    Предварительные настройки


    Сперва давайте выключим JUnit 4, который поставляется в SpringBoot 2 по-умолчанию, и включим JUnit 5.


    Для этого внесем изменения в pom.xml:


    <dependencies>
        <!--...-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>junit</groupId>
                    <artifactId>junit</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>5.3.2</version>
            <scope>test</scope>
        </dependency>
        <!--...-->
    </dependencies>

    Предполагаемое решение


    Мы хотим аннотировать каждый тест простой аннотацией со свойством, указывающим на то, включен ли тест или нет. Напомню, что значения этого свойства мы собираемся хранить в файле application.properties.


    Аннотация


    Создадим аннотацию:


    @Retention(RetentionPolicy.RUNTIME)
    @ExtendWith(TestEnabledCondition.class)
    public @interface TestEnabled {
        String property();
    }

    Обработка аннотации


    Без обработчика аннотации не обойтись.


    public class TestEnabledCondition implements ExecutionCondition {
    
        @Override
        public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) {
            Optional<TestEnabled> annotation = context.getElement().map(e -> e.getAnnotation(TestEnabled.class));
    
            return context.getElement()
                            .map(e -> e.getAnnotation(TestEnabled.class))
                            .map(annotation -> {
                                String property = annotation.property();
    
                                return Optional.ofNullable(environment.getProperty(property, Boolean.class))
                                        .map(value -> {
                                            if (Boolean.TRUE.equals(value)) {
                                                return ConditionEvaluationResult.enabled("Enabled by property: "+property);
                                            } else {
                                                return ConditionEvaluationResult.disabled("Disabled by property: "+property);
                                            }
                                        }).orElse(
                                                ConditionEvaluationResult.disabled("Disabled - property <"+property+"> not set!")
                                        );
                            }).orElse(
                                    ConditionEvaluationResult.enabled("Enabled by default")
                            );
        }
    }

    Необходимо создать класс (без аннотации Spring-а @Component), который реализует интерфейс ExecutionCondition.


    В этом классе необходимо реализовать один метод этого интерфейса — ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context).


    Этот метод принимает контекст выполняемого JUnit теста и возвращает условие — должен ли тест быть запущен или нет.


    Прочитать подробней по условное выполнение тестов JUnit5 можно в официальной документации.


    Но как нам проверить значение свойства, которое прописано в application.properties в таком случае?


    Получение доступа к контексту Spring из контекста JUnit


    Вот таким образом мы можем получить окружение Spring, с которым был запущен наш JUnit тест, из ExtensionContext.


    Environment environment = SpringExtension.getApplicationContext(context).getEnvironment();

    Можете взглянуть на полный код класса TestEnabledCondition.


    Создадим тесты


    Давайте создадим несколько тестов и попробуем управлять их запуском:


    @SpringBootTest
    public class SkiptestApplicationTests {
    
        @TestEnabled(property = "app.skip.test.first")
        @Test
        public void testFirst() {
            assertTrue(true);
        }
    
        @TestEnabled(property = "app.skip.test.second")
        @Test
        public void testSecond() {
            assertTrue(false);
        }
    
    }

    Наш application.properties файл при этом выглядит так:


    app.skip.test.first=true
    app.skip.test.second=false

    Итак...


    Результат запуска:



    Следующий шаг — отделим префиксы наших свойств в аннотацию класса


    Писать перед каждым тестом полные названия свойств из application.properties — утомительное занятие. Поэтому резонно их префикс вынести на уровень класса тестов — в отдельную аннотацию.


    Создадим annotation для хранения префиксов — TestEnabledPrefix:


    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface TestEnabledPrefix {
        String prefix();
    }

    Обработка и использование аннотации TestEnabledPrefix


    Приступим к обработке новой аннотации.


    Давайте создадим вспомогательный класс AnnotationDescription


    С помощью этого класса мы сможем хранить имя свойства из application.properties и его значение.


    public class TestEnabledCondition implements ExecutionCondition {
    
        static class AnnotationDescription {
            String name;
            Boolean annotationEnabled;
            AnnotationDescription(String prefix, String property) {
                this.name = prefix + property;
            }
            String getName() {
                return name;
            }
            AnnotationDescription setAnnotationEnabled(Boolean value) {
                this.annotationEnabled = value;
                return this;
            }
            Boolean isAnnotationEnabled() {
                return annotationEnabled;
            }
        }
    
        /* ... */
    }

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


    Создадим метод, который извлечет нам значение свойства "префикс" из аннотации класса TestEnabledPrefix


    public class TestEnabledCondition implements ExecutionCondition {
    
        /* ... */
    
        private AnnotationDescription makeDescription(ExtensionContext context, String property) {
            String prefix = context.getTestClass()
                    .map(cl -> cl.getAnnotation(TestEnabledPrefix.class))
                    .map(TestEnabledPrefix::prefix)
                    .map(pref -> !pref.isEmpty() && !pref.endsWith(".") ? pref + "." : "")
                    .orElse("");
            return new AnnotationDescription(prefix, property);
        }
    
        /* ... */
    
    }

    И теперь проверим значение свойства из application.properties по имени, указанном в аннотации теста


    public class TestEnabledCondition implements ExecutionCondition {
    
        /* ... */
    
        @Override
        public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) {
            Environment environment = SpringExtension.getApplicationContext(context).getEnvironment();
    
            return context.getElement()
                    .map(e -> e.getAnnotation(TestEnabled.class))
                    .map(TestEnabled::property)
                    .map(property -> makeDescription(context, property))
                    .map(description -> description.setAnnotationEnabled(environment.getProperty(description.getName(), Boolean.class)))
                    .map(description -> {
                        if (description.isAnnotationEnabled()) {
                            return ConditionEvaluationResult.enabled("Enabled by property: "+description.getName());
                        } else {
                            return ConditionEvaluationResult.disabled("Disabled by property: "+description.getName());
                        }
                    }).orElse(
                            ConditionEvaluationResult.enabled("Enabled by default")
                    );
    
        }
    
    }


    Полный код класса доступен по ссылке.


    Использование новой аннотации


    Теперь применим нашу аннотацию к тест-классу:


    @SpringBootTest
    @TestEnabledPrefix(property = "app.skip.test")
    public class SkiptestApplicationTests {
    
        @TestEnabled(property = "first")
        @Test
        public void testFirst() {
            assertTrue(true);
        }
    
        @TestEnabled(property = "second")
        @Test
        public void testSecond() {
            assertTrue(false);
        }
    
    }

    Теперь наш код тестов стал чище и проще.


    Хочу выразить благодарность пользователям reddit-а за их советы:


    1) dpash за совет
    2) BoyRobot777 за совет


    P.S.


    Статья является авторским переводом. Английский вариант опубликован в README.md файле рядом с кодом проекта.

    • +10
    • 2,1k
    • 9
    Поддержать автора
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 9

      0
      А не проще на TestNG перейти? Не, ну реально — что есть в JUnit такого, чего нет в TestNG?

      Ассерты все равно удобнее на hamcrest писать.

      А группы тестов там есть (насколько я помню — давно), они включаются и выключаются при запуске.
        +1
        «Перейти» не всегда проще.

        К тому же, некоторые статьи пишутся для proof of concept.
          0
          >Перейти» не всегда проще.
          Вы не поверите — я пробовал. TestNG появился в 2004 — вот тогда же и попробовал. Вот посмотрите на этот код, и скажите мне, отличается ли он чем-то от JUnit?

          package example1;
           
          import org.testng.annotations.*;
           
          public class SimpleTest {
           
           @BeforeClass
           public void setUp() {
             // code that will be invoked when this test is instantiated
           }
           
           @Test(groups = { "fast" })
           public void aFastTest() {
             System.out.println("Fast test");
           }
           
           @Test(groups = { "slow" })
           public void aSlowTest() {
              System.out.println("Slow test");
           }
           
          }
          


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

          proof of concept — ну это вполне понятно, даже ради удовлетворения любопытства. Я в общем-то и хочу понять, дает ли это в итоге что-то принципиально такое, чего не было ранее, уже много лет?
            –1
            С «перейти» бычно проблема в «легаси», которое существует на момент перехода.
              0
              С легаси — то есть с написанными тестами? Ну понятно, что усилия придется потратить. Ну так предлагаемое решение — оно ведь тоже далеко не бесплатное. @TestEnabled(property = «first») — оно ведь само в код не добавится, его нужно ручками вписать везде где нужно. И никакая IDE за нас не решит, где именно нам нужно.

              На первый взгляд — усилия сопоставимые. Впрочем, я никого не агитирую, хотя TestNG — хорошая, проверенная временем штука.

                +2
                Скажем так: у меня есть опыт работы с несколькими юнит-тест-фреймворками в одном проекте и повторять его я не особо хочу:)

                А дальше всё зависит от того сколько старых юнит-тестов надо переписать и сколько проперти-атрибутов надо добавить в код. И если соотношение несколько тысяч к нескольким десяткам, то, процитирую ещё раз:
                Перейти» не всегда проще.

                :)
                  0
                  >Скажем так: у меня есть опыт работы с несколькими юнит-тест-фреймворками в одном проекте
                  У меня тоже. Негативных эффектов не припомню (но агитировать просто так ради развлечения — не стану тоже. Оно того не стоит — эти фреймворки на мой взгляд почти равноценны). Я бы еще подумал, если бы второй фреймворк был скажем типа property based, ну или что-то типа мутаций тестов бы делал. Тогда может быть была бы некая польза.

                  >Перейти не всегда проще.

                  Так я не спорю с этим утверждением в целом. Я просто оценил на глаз для частного случая стоимость того и другого. Выглядит примерно одинаково. Я запросто мог чего-то не учесть, но опыт перехода на TestNG у меня был тоже — и такой переход совсем не сложный, особенно если мы можем в спокойной обстановке переводить один тест за другим.

                  Более того (цитирую документацию):

                  TestNG can automatically recognize and run JUnit tests, so you can use TestNG as a runner for all your existing tests and write new tests using TestNG.

                  Ну то есть, в оптимистичном случае переписывать вообще не нужно. Но это не я вам обещал, а Цедрик ;)
        +1

        Хм, Spring Boot… а почему не используем SpringRunner и @IfProfileValue?

          0

          @IfProfileValue не используется, потому что:


          app.skip.test.third=false

          @IfProfileValue(name = "app.skip.test.third", value = "true")
              @Test
              public void testThird() {
                  assertTrue(false);
              }

          Результат:


          org.opentest4j.AssertionFailedError: expected: <true> but was: <false>

          А SpringRunner же нужен для автовайринга. Для демо-примера не используется.

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

        Самое читаемое