Ръководство за JUnit 5

1. Общ преглед

JUnit е една от най-популярните модулни тестови рамки в екосистемата на Java. Версията JUnit 5 съдържа редица вълнуващи иновации, целящи да поддържат нови функции в Java 8 и по-нови версии , както и да позволяват много различни стилове на тестване.

2. Зависимости на Maven

Настройването на JUnit 5.x.0 е доста лесно, трябва да добавим следната зависимост към нашия pom.xml :

 org.junit.jupiter junit-jupiter-engine 5.1.0 test 

Важно е да се отбележи, че тази версия изисква Java 8 да работи .

Нещо повече, сега има директна поддръжка за стартиране на модулни тестове на платформата JUnit в Eclipse, както и IntelliJ. Можете, разбира се, да провеждате тестове, като използвате целта на Maven Test.

От друга страна, IntelliJ поддържа JUnit 5 по подразбиране. Следователно, стартирането на JUnit 5 на IntelliJ е доста просто, просто щракнете с десния бутон -> Run или Ctrl-Shift-F10.

3. Архитектура

JUnit 5 се състои от няколко различни модула от три различни подпроекта:

3.1. Платформа JUnit

Платформата е отговорна за стартирането на тестови рамки на JVM. Той определя стабилен и мощен интерфейс между JUnit и неговия клиент, като инструменти за изграждане.

Крайната цел е как клиентите му да се интегрират лесно с JUnit при откриването и изпълнението на тестовете.

Той също така определя API TestEngine за разработване на рамка за тестване, която работи на платформата JUnit. По този начин можете да включите библиотеки за тестване на трети страни директно в JUnit, като внедрите персонализиран TestEngine.

3.2. Юнит Юпитер

Този модул включва нови модели за програмиране и разширение за писане на тестове в JUnit 5. Новите анотации в сравнение с JUnit 4 са:

  • @TestFactory - означава метод, който е фабрика за тестове за динамични тестове
  • @DisplayName - дефинира потребителско име за тестов клас или метод за тестване
  • @Nested - означава, че анотираният клас е вложен, нестатичен тестов клас
  • @Tag - декларира тагове за филтриране на тестове
  • @ExtendWith - използва се за регистриране на персонализирани разширения
  • @BeforeEach - означава, че анотираният метод ще бъде изпълнен преди всеки метод за тестване (преди това @Before )
  • @AfterEach - означава, че анотираният метод ще бъде изпълнен след всеки тест метод (преди @After )
  • @BeforeAll - означава, че анотираният метод ще бъде изпълнен преди всички тестови методи в текущия клас (преди това @BeforeClass )
  • @AfterAll - означава, че анотираният метод ще бъде изпълнен след всички тестови методи в текущия клас (преди това @AfterClass )
  • @Disable - използва се за деактивиране на тестов клас или метод (по-рано @Ignore )

3.3. JUnit Vintage

Поддържа текущи JUnit 3 и JUnit 4 тестове на платформата JUnit 5.

4. Основни анотации

За да обсъдим нови анотации, разделихме раздела на следните групи, отговорни за изпълнението: преди тестовете, по време на тестовете (по избор) и след тестовете:

4.1. @BeforeAll и @BeforeEach

По-долу е даден пример за простия код, който трябва да бъде изпълнен преди основните тестови случаи:

@BeforeAll static void setup() { log.info("@BeforeAll - executes once before all test methods in this class"); } @BeforeEach void init() { log.info("@BeforeEach - executes before each test method in this class"); }

Важно е да се отбележи, че методът с @BeforeAll анотация трябва да бъде статичен, в противен случай кодът няма да се компилира.

4.2. @DisplayName и @Disabled

Нека да преминем към нови методи по избор:

@DisplayName("Single test successful") @Test void testSingleSuccessTest() { log.info("Success"); } @Test @Disabled("Not implemented yet") void testShowSomething() { }

Както виждаме, можем да променим показваното име или да деактивираме метода с коментар, като използваме нови пояснения.

4.3. @AfterEach и @AfterAll

И накрая, нека обсъдим методи, свързани с операции след изпълнение на тестове:

@AfterEach void tearDown() { log.info("@AfterEach - executed after each test method."); } @AfterAll static void done() { log.info("@AfterAll - executed after all test methods."); }

Моля, обърнете внимание, че методът с @AfterAll също трябва да бъде статичен метод.

5. Твърдения и предположения

JUnit 5 се опитва да се възползва изцяло от новите функции на Java 8, особено ламбда изрази.

5.1. Твърдения

Твърденията са преместени в org.junit.jupiter.api.Assertions и са значително подобрени. Както споменахме по-рано, вече можете да използвате ламбда в твърдения:

@Test void lambdaExpressions() { assertTrue(Stream.of(1, 2, 3) .stream() .mapToInt(i -> i) .sum() > 5, () -> "Sum should be greater than 5"); }

Въпреки че горният пример е тривиален, едно предимство от използването на ламбда израза за съобщението за твърдение е, че то се оценява мързеливо, което може да спести време и ресурси, ако изграждането на съобщението е скъпо.

It is also now possible to group assertions with assertAll() which will report any failed assertions within the group with a MultipleFailuresError:

 @Test void groupAssertions() { int[] numbers = {0, 1, 2, 3, 4}; assertAll("numbers", () -> assertEquals(numbers[0], 1), () -> assertEquals(numbers[3], 3), () -> assertEquals(numbers[4], 1) ); }

This means it is now safer to make more complex assertions, as you will be able to pinpoint the exact location of any failure.

5.2. Assumptions

Assumptions are used to run tests only if certain conditions are met. This is typically used for external conditions that are required for the test to run properly, but which are not directly related to whatever is being tested.

You can declare an assumption with assumeTrue(), assumeFalse(), and assumingThat().

@Test void trueAssumption() { assumeTrue(5 > 1); assertEquals(5 + 2, 7); } @Test void falseAssumption() { assumeFalse(5  assertEquals(2 + 2, 4) ); }

If an assumption fails, a TestAbortedException is thrown and the test is simply skipped.

Assumptions also understand lambda expressions.

6. Exception Testing

There are two ways of exception testing in JUnit 5. Both of them can be implemented by using assertThrows() method:

@Test void shouldThrowException() { Throwable exception = assertThrows(UnsupportedOperationException.class, () -> { throw new UnsupportedOperationException("Not supported"); }); assertEquals(exception.getMessage(), "Not supported"); } @Test void assertThrowsException() { String str = null; assertThrows(IllegalArgumentException.class, () -> { Integer.valueOf(str); }); }

The first example is used to verify more detail of the thrown exception and the second one just validates the type of exception.

7. Test Suites

To continue the new features of JUnit 5, we will try to get to know the concept of aggregating multiple test classes in a test suite so that we can run those together. JUnit 5 provides two annotations: @SelectPackages and @SelectClasses to create test suites.

Keep in mind that at this early stage most IDEs do not support those features.

Let's have a look at the first one:

@RunWith(JUnitPlatform.class) @SelectPackages("com.baeldung") public class AllUnitTest {}

@SelectPackage is used to specify the names of packages to be selected when running a test suite. In our example, it will run all test. The second annotation, @SelectClasses, is used to specify the classes to be selected when running a test suite:

@RunWith(JUnitPlatform.class) @SelectClasses({AssertionTest.class, AssumptionTest.class, ExceptionTest.class}) public class AllUnitTest {}

For example, above class will create a suite contains three test classes. Please note that the classes don't have to be in one single package.

8. Dynamic Tests

The last topic that we want to introduce is JUnit 5 Dynamic Tests feature, which allows to declare and run test cases generated at run-time. In contrary to the Static Tests which defines fixed number of test cases at the compile time, the Dynamic Tests allow us to define the tests case dynamically in the runtime.

Dynamic tests can be generated by a factory method annotated with @TestFactory. Let's have a look at the code example:

@TestFactory public Stream translateDynamicTestsFromStream() { return in.stream() .map(word -> DynamicTest.dynamicTest("Test translate " + word, () -> { int id = in.indexOf(word); assertEquals(out.get(id), translate(word)); }) ); }

This example is very straightforward and easy to understand. We want to translate words using two ArrayList, named in and out, respectively. The factory method must return a Stream, Collection, Iterable, or Iterator. In our case, we choose Java 8 Stream.

Please note that @TestFactory methods must not be private or static. The number of tests is dynamic, and it depends on the ArrayList size.

9. Conclusion

Записът представлява кратък преглед на промените, които идват с JUnit 5.

Виждаме, че JUnit 5 има голяма промяна в своята архитектура, свързана с стартер на платформа, интеграция с инструмент за изграждане, IDE, други модулни тестови рамки и др. Освен това JUnit 5 е по-интегриран с Java 8, особено с Lambdas и Stream концепции .

Примерите, използвани в тази статия, могат да бъдат намерени в проекта GitHub.