Ръководство за динамични тестове в Junit 5

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

Динамичното тестване е нов програмен модел, въведен в JUnit 5. В тази статия ще разгледаме какво точно представляват динамичните тестове и как да ги създадем.

Ако сте напълно нови за JUnit 5, може да искате да проверите визуализацията на JUnit 5 и нашето основно ръководство.

2. Какво е DynamicTest ?

Стандартните тестове, анотирани с анотация @Test, са статични тестове, които са напълно уточнени по време на компилиране. А DynamicTest е тест, генерирани по време на изпълнение . Тези тестове се генерират от фабричен метод, коментиран с анотацията @TestFactory .

А @TestFactory метод трябва да връща поток , Събиране , Iterable или Итераторът на DynamicTest случаи. Връщането на всичко друго ще доведе до JUnitException, тъй като невалидните типове връщане не могат да бъдат открити по време на компилация. Отделно от това, на @TestFactory метод не може да бъде Стати в или частен .

На DynamicTest те се изпълняват по различен от стандартния @Test те и не поддържат жизнения цикъл обратни повиквания. Това означава, че методите @BeforeEach и @AfterEach няма да бъдат извикани за DynamicTest s .

3. Създаване на DynamicTests

Първо, нека да разгледаме различните начини за създаване на DynamicTest s.

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

Ще създадем колекция от DynamicTest :

@TestFactory Collection dynamicTestsWithCollection() { return Arrays.asList( DynamicTest.dynamicTest("Add test", () -> assertEquals(2, Math.addExact(1, 1))), DynamicTest.dynamicTest("Multiply Test", () -> assertEquals(4, Math.multiplyExact(2, 2)))); }

Методът @TestFactory казва на JUnit, че това е фабрика за създаване на динамични тестове. Както виждаме, връщаме само колекция от DynamicTest . Всеки от DynamicTest се състои от две части, името на теста или името на дисплея и изпълним файл .

Резултатът ще съдържа името на дисплея, което предадохме на динамичните тестове:

Add test(dynamicTestsWithCollection()) Multiply Test(dynamicTestsWithCollection())

Същият тест може да бъде модифициран, за да върне Iterable , Iterator или Stream :

@TestFactory Iterable dynamicTestsWithIterable() { return Arrays.asList( DynamicTest.dynamicTest("Add test", () -> assertEquals(2, Math.addExact(1, 1))), DynamicTest.dynamicTest("Multiply Test", () -> assertEquals(4, Math.multiplyExact(2, 2)))); } @TestFactory Iterator dynamicTestsWithIterator() { return Arrays.asList( DynamicTest.dynamicTest("Add test", () -> assertEquals(2, Math.addExact(1, 1))), DynamicTest.dynamicTest("Multiply Test", () -> assertEquals(4, Math.multiplyExact(2, 2)))) .iterator(); } @TestFactory Stream dynamicTestsFromIntStream() { return IntStream.iterate(0, n -> n + 2).limit(10) .mapToObj(n -> DynamicTest.dynamicTest("test" + n, () -> assertTrue(n % 2 == 0))); }

Моля, обърнете внимание, че ако @TestFactory върне поток , той ще бъде автоматично затворен, след като всички тестове бъдат изпълнени.

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

4. Създаване на поток от DynamicTests

За целите на демонстрацията помислете за DomainNameResolver, който връща IP адрес, когато предадем името на домейна като вход.

За улеснение, нека да разгледаме скелета на високо ниво на нашия фабричен метод:

@TestFactory Stream dynamicTestsFromStream() { // sample input and output List inputList = Arrays.asList( "www.somedomain.com", "www.anotherdomain.com", "www.yetanotherdomain.com"); List outputList = Arrays.asList( "154.174.10.56", "211.152.104.132", "178.144.120.156"); // input generator that generates inputs using inputList /*...code here...*/ // a display name generator that creates a // different name based on the input /*...code here...*/ // the test executor, which actually has the // logic to execute the test case /*...code here...*/ // combine everything and return a Stream of DynamicTest /*...code here...*/ }

Тук няма много код, свързан с DynamicTest , освен анотацията @TestFactory , с която вече сме запознати.

Двата ArrayList ще бъдат използвани като вход за DomainNameResolver и съответно очакван изход.

Нека сега да разгледаме входния генератор:

Iterator inputGenerator = inputList.iterator();

Входният генератор не е нищо друго освен итератор на низ . Той използва нашия inputList и връща името на домейна едно по едно.

Генераторът на показваните имена е доста прост:

Function displayNameGenerator = (input) -> "Resolving: " + input;

Задачата на генератора на показвани имена е просто да предостави име на дисплей за тестовия случай, който ще се използва в отчетите JUnit или в раздела JUnit на нашата IDE.

Тук ние просто използваме името на домейна, за да генерираме уникални имена за всеки тест. Не се изисква създаване на уникални имена, но ще помогне в случай на неуспех. Разполагайки с това, ще можем да кажем името на домейна, за който тестовият случай е неуспешен.

Сега нека да разгледаме централната част на нашия тест - кода за изпълнение на теста:

DomainNameResolver resolver = new DomainNameResolver(); ThrowingConsumer testExecutor = (input) -> { int id = inputList.indexOf(input); assertEquals(outputList.get(id), resolver.resolveDomain(input)); };

Използвахме ThrowingConsumer , който е @FunctionalInterface за писане на тестовия случай. За всеки вход, генериран от генератора на данни, извличаме очаквания изход от outputList и действителния изход от екземпляр на DomainNameResolver .

Сега последната част е просто да се съберат всички парчета и да се върне като поток на DynamicTest :

return DynamicTest.stream( inputGenerator, displayNameGenerator, testExecutor);

Това е. Изпълнението на теста ще покаже отчета, съдържащ имената, дефинирани от нашия генератор на показвани имена:

Resolving: www.somedomain.com(dynamicTestsFromStream()) Resolving: www.anotherdomain.com(dynamicTestsFromStream()) Resolving: www.yetanotherdomain.com(dynamicTestsFromStream())

5. Подобряване на DynamicTest с помощта на функции Java 8

Тестовата фабрика, написана в предишния раздел, може да бъде драстично подобрена с помощта на функциите на Java 8. Резултантният код ще бъде много по-чист и може да бъде написан в по-малък брой редове:

@TestFactory Stream dynamicTestsFromStreamInJava8() { DomainNameResolver resolver = new DomainNameResolver(); List domainNames = Arrays.asList( "www.somedomain.com", "www.anotherdomain.com", "www.yetanotherdomain.com"); List outputList = Arrays.asList( "154.174.10.56", "211.152.104.132", "178.144.120.156"); return inputList.stream() .map(dom -> DynamicTest.dynamicTest("Resolving: " + dom, () -> {int id = inputList.indexOf(dom); assertEquals(outputList.get(id), resolver.resolveDomain(dom)); })); }

Горният код има същия ефект като този, който видяхме в предишния раздел. В inputList.stream (). Карта () осигурява потока на входа (вход генератор). Първият аргумент на dynamicTest () е нашият генератор на показвани имена (“Разрешаване:” + dom ), докато вторият аргумент, ламбда , е нашият изпълнител на теста.

Резултатът ще бъде същият като този от предишния раздел.

6. Допълнителен пример

В този пример ние допълнително проучваме силата на динамичните тестове за филтриране на входовете въз основа на тестовите случаи:

@TestFactory Stream dynamicTestsForEmployeeWorkflows() { List inputList = Arrays.asList( new Employee(1, "Fred"), new Employee(2), new Employee(3, "John")); EmployeeDao dao = new EmployeeDao(); Stream saveEmployeeStream = inputList.stream() .map(emp -> DynamicTest.dynamicTest( "saveEmployee: " + emp.toString(), () -> { Employee returned = dao.save(emp.getId()); assertEquals(returned.getId(), emp.getId()); } )); Stream saveEmployeeWithFirstNameStream = inputList.stream() .filter(emp -> !emp.getFirstName().isEmpty()) .map(emp -> DynamicTest.dynamicTest( "saveEmployeeWithName" + emp.toString(), () -> { Employee returned = dao.save(emp.getId(), emp.getFirstName()); assertEquals(returned.getId(), emp.getId()); assertEquals(returned.getFirstName(), emp.getFirstName()); })); return Stream.concat(saveEmployeeStream, saveEmployeeWithFirstNameStream); }

The save(Long) method needs only the employeeId. Hence, it utilizes all the Employee instances. The save(Long, String) method needs firstName apart from the employeeId. Hence, it filters out the Employee instances without firstName.

Finally, we combine both the streams and return all the tests as a single Stream.

Now, let's have a look at the output:

saveEmployee: Employee [id=1, firstName=Fred](dynamicTestsForEmployeeWorkflows()) saveEmployee: Employee [id=2, firstName=](dynamicTestsForEmployeeWorkflows()) saveEmployee: Employee [id=3, firstName=John](dynamicTestsForEmployeeWorkflows()) saveEmployeeWithNameEmployee [id=1, firstName=Fred](dynamicTestsForEmployeeWorkflows()) saveEmployeeWithNameEmployee [id=3, firstName=John](dynamicTestsForEmployeeWorkflows())

7. Conclusion

The parameterized tests can replace many of the examples in this article. However, the dynamic tests differ from the parameterized tests as they support full test lifecycle, while parametrized tests don't.

Освен това динамичните тестове осигуряват по-голяма гъвкавост по отношение на начина, по който се генерират входните данни и как се изпълняват тестовете.

JUnit 5 предпочита разширенията пред принципа на функциите. В резултат на това основната цел на динамичните тестове е да предоставят точка за разширение за рамки или разширения на трети страни.

Можете да прочетете повече за други функции на JUnit 5 в нашата статия за многократни тестове в JUnit 5.

Не забравяйте да разгледате пълния изходен код на тази статия в GitHub.