JUnit 5 je ďalšou generáciou JUnit-u. Vytvára nový základ pre vývojárov a QA inžinierov. Je určený pre testovanie na JVM. Celý je postavený na Java 8 a vyšsie. Tím JUnit 5 vydal Milestone 3 dňa 30. novembra 2016 a v súčasnosti pracuje na ďalších míľnikoch a všeobecne dostupnom release.
Porovnanie JUnit 4 a JUnit 5 v skratke
JUnit 4
- Release už pred desiatimi rokmi
- Distribuované formou – všetko v jednom jar súbore
- Údržba a update sa stáva po 10 rokoch takmer neudržateľná
JUnit 5
- Distribuované vo viacerých jar súboroch:
- JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
- Java 8+
- JUnit Platform slúži ako základ pre spustenie testov na JVM
- JUnit Jupiter je kombinácia nového programovacieho modelu a rozšírenie modelu pre písanie testov
- JUnit Vintage poskytuje TestEngine pre spustenie testov založených na starších verziách JUnit 3 a JUnit 4
Prvé dotyky s JUnit 5
Pre JUnit 5 – Milestone 3, je integrácia s Java IDE plne k dispozícii len pre IntelliJ. Ostatné IDE ako Eclipse, tam sa integrácia zatiaľ vykonáva pomocou maven-surefire-plugin. Pozrime sa na pom.xml, ktoré v tomto tvare zaručí úspešný štart našich prvých experimentov s JUnit 5.
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>junit5-pg</groupId> <artifactId>junit5-pg</artifactId> <version>1.0-SNAPSHOT</version> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <java.version>1.8</java.version> <junit.version>4.12</junit.version> <junit.jupiter.version>5.0.0-M3</junit.jupiter.version> <junit.vintage.version>${junit.version}.0-M3</junit.vintage.version> <junit.platform.version>1.0.0-M3</junit.platform.version> </properties> <build> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>3.1</version> <configuration> <source>${java.version}</source> <target>${java.version}</target> </configuration> </plugin> <plugin> <artifactId>maven-surefire-plugin</artifactId> <version>2.19.1</version> <configuration> <includes> <include>**/Test*.java</include> <include>**/*Test.java</include> <include>**/*Tests.java</include> <include>**/*TestCase.java</include> </includes> <properties> <!-- <includeTags>fast</includeTags> --> <excludeTags>slow</excludeTags> </properties> </configuration> <dependencies> <dependency> <groupId>org.junit.platform</groupId> <artifactId>junit-platform-surefire-provider</artifactId> <version>${junit.platform.version}</version> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-engine</artifactId> <version>${junit.jupiter.version}</version> </dependency> <dependency> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> <version>${junit.vintage.version}</version> </dependency> </dependencies> </plugin> </plugins> </build> <dependencies> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-api</artifactId> <version>${junit.jupiter.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>${junit.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.junit.platform</groupId> <artifactId>junit-platform-runner</artifactId> <version>${junit.platform.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> <version>${junit.vintage.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.junit.platform</groupId> <artifactId>junit-platform-launcher</artifactId> <version>1.0.0-M3</version> </dependency> <dependency> <groupId>org.junit.platform</groupId> <artifactId>junit-platform-console</artifactId> <version>1.0.0-M3</version> </dependency> </dependencies> </project>
Začneme s testom triedy Calculator.java
package com.example; public class Calculator { public int add(int a, int b) { return a + b; } public int subtraction(int a, int b) { return a - b; } }
Prvá JUnit 5 testovacia trieda, FirstTest.java
package com.example.test; import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import org.junit.jupiter.api.*; import com.example.Calculator; @Tag("fast") class FirstTest { static String staticSemafor = null; String semafor = null; @BeforeAll static void beforeAll() { staticSemafor = "abc"; System.out.println("before all stuff..."); } @AfterAll static void afterAll() { System.out.println("after all stuff..."); } @BeforeEach void beforeEach() { this.semafor = "def"; System.out.println("...before each stuff..."); } @AfterEach void aftreEach() { System.out.println("...after each stuff..."); } @Test @DisplayName("My 1st JUnit 5 test! 😎") void myFirstTest(TestInfo testInfo) { Calculator calculator = new Calculator(); assertEquals(2, calculator.add(1, 1), "1 + 1 should equal 2"); assertEquals("My 1st JUnit 5 test! 😎", testInfo.getDisplayName(), () -> "TestInfo is injected correctly"); } @Test @DisplayName("Semafor and calculator Test") void semaforTest(TestInfo testInfo) { Calculator calculator = new Calculator(); assertAll("All assertions", () -> assertNotNull(FirstTest.staticSemafor), () -> assertNotNull(this.semafor), () -> assertEquals(2, calculator.add(1, 1), "1 + 1 should equal 2"), () -> assertEquals("Semafor and calculator Test", testInfo.getDisplayName(), () -> "TestInfo is injected correctly") ); } @Disabled @Test @DisplayName("My Disabled JUnit 5 test!") void myDisabledTest(TestInfo testInfo) { Calculator calculator = new Calculator(); assertEquals(2, calculator.add(1, 1), "1 + 1 should equal 2"); } }
A takto vyzerajú výsledky testu…
Nové anotácie JUnit 5
- @Tag – sa používa na označovanie testov, ktorý je neskôr použiteľný na filtrovanie spúšťaných testov cez pom.xml. Táto anotácia sa používa buď na úrovni triedy alebo metódy. Napríklad ako @Category(SmokeTests.class) v JUnit 4
- @DisplayName – Deklaruje vlastný názov pre testovaciu triedu alebo testovaciu metódu. Konečne J
- @BeforeAll – analogicky k JUnit 4 @BeforeClass
- @AfterAll – analogicky k JUnit 4 @AfterClass
- @BeforeEach – analogicky k JUnit 4 @Before
- @AfterEach – analogicky k JUnit 4 @After
- @Disabled – analogicky k JUnit 4 @Ignore
- TestInfo – je priama náhrada za “TestName” z JUnit 4.
- assertAll – nové varianty Assertions.assertAll (), ktoré akceptujú Java 8 streamy, napr.: Stream<Executable>
Migrácia testov z JUnit 4 na JUnit 5 by teda mohla vyzerať nasledovne
- @Before a @After => @BeforeEach a @AfterEach
- @BeforeClass a @AfterClass => @BeforeAll a @AfterAll
- @Ignore => @Disabled
- @Category => @Tag
- @RunWith => @ExtendWith.
- @Rule a @ClassRule => @ExtendWith
- @Test(timeout = 1000) anotácia už nie je podporovaná
V nasledujúcom texte si ukážeme základné rozdiely pre testovanie timeout a náhradu za @Rule a @ClassRule
Migrácia pre timeout testovanie
JUnit 4:
@Test(timeout@Test(timeout=100) public void sleep100() { Thread.sleep(100); } JUnit 5:
@Test void sleep100() { assertTimeout(ofMillis(1), () -> { Thread.sleep(100); }); }
Migrácia prostredníctvom @ExtendWith
Túto novú feature si ukážeme na triede TimingExtension.java, ktorá poskytuje údaje o trvaní behu jednotlivých testov.
import java.lang.reflect.Method; import java.util.logging.Logger; import org.junit.jupiter.api.extension.AfterTestExecutionCallback; import org.junit.jupiter.api.extension.BeforeTestExecutionCallback; import org.junit.jupiter.api.extension.ExtensionContext.Namespace; import org.junit.jupiter.api.extension.ExtensionContext.Store; import org.junit.jupiter.api.extension.TestExtensionContext; public class TimingExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback { private static final Logger LOG = Logger.getLogger(TimingExtension.class.getName()); @Override public void beforeTestExecution(TestExtensionContext context) throws Exception { getStore(context).put(context.getTestMethod().get(), System.currentTimeMillis()); } @Override public void afterTestExecution(TestExtensionContext context) throws Exception { Method testMethod = context.getTestMethod().get(); long start = getStore(context).remove(testMethod, long.class); long duration = System.currentTimeMillis() - start; LOG.info(() -> String.format("Method [%s] took %s ms.", testMethod.getName(), duration)); } private Store getStore(TestExtensionContext context) { return context.getStore(Namespace.create(getClass(), context)); } }
A tu je použite TimingExtension v triede TimingExtensionTest.java
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @ExtendWith(TimingExtension.class) @DisplayName("Timing test example") public class TimingExtensionTests { @Test @DisplayName("Sleep for 20 [ms]") void sleep20ms() throws Exception { Thread.sleep(20); } @Test @DisplayName("Sleep for 50 [ms]") void sleep50ms() throws Exception { Thread.sleep(50); } }
Výsledok behu takýchto testov potom vyzerá takto:
Nové vlastnosti JUnit 5 – TestReporter
TestReporter umožňuje prezentáciu informácií o práve bežiacich testoch. Pozrite si príklad.
import java.util.HashMap; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.TestReporter; class TestReporterDemo { @Test void reportSingleValue(TestReporter testReporter) { testReporter.publishEntry("a key", "a value"); } @Test @DisplayName("Report Several Values") void reportSeveralValues(TestReporter testReporter, TestInfo ti) { HashMap<String, String> values = new HashMap<>(); values.put("user name", "dk38"); values.put("award year", "1974"); testReporter.publishEntry(values); testReporter.publishEntry("Test", ti.getDisplayName()); } }
Nové vlastnosti JUnit 5 – Nested tests
Nested testy umožňujú vyjadrovať vzťahy medzi niekoľkými skupinami testov. Pozrite si príklad
import org.junit.jupiter.api.*; import java.util.ArrayList; import java.util.EmptyStackException; import java.util.List; import java.util.Stack; import static java.time.Duration.ofMillis; import static org.junit.jupiter.api.Assertions.*; @DisplayName("A stack") @Tag("fast") public class AStackTest { Stack<Object> stack; List<String> list; @Test @DisplayName("is instantiated with new Stack()") void isInstantiatedWithNew() { new Stack<>(); } @Nested @DisplayName("when new") class WhenNew { @BeforeEach void createNewStack() { stack = new Stack<>(); System.out.println("BeforeEach... stack init"); } @Test @DisplayName("is empty") void isEmpty() { assertTrue(stack.isEmpty()); } @Test @DisplayName("throws EmptyStackException when popped") void throwsExceptionWhenPopped() { assertThrows(EmptyStackException.class, () -> stack.pop()); } @Test @DisplayName("throws EmptyStackException when peeked") void throwsExceptionWhenPeeked() { assertThrows(EmptyStackException.class, () -> stack.peek()); } @Nested @DisplayName("after pushing an element") class AfterPushing { String anElement = "an element"; @BeforeEach void pushAnElement() { stack.push(anElement); } @Test @DisplayName("it is no longer empty") void isNotEmpty() { assertFalse(stack.isEmpty()); } @Test void timeoutExceeded() { assertTimeout(ofMillis(1), () -> { assertAll("All", () -> assertEquals(anElement, stack.pop()), () -> assertTrue(stack.isEmpty()) ); }); } @Test @DisplayName("returns the element when popped and is empty") void returnElementWhenPopped(TestInfo testInfo) { assertAll("Return Element When Peeked", () -> assertEquals("returns the element when popped and is empty", testInfo.getDisplayName(), () -> "TestInfo is injected correctly"), () -> assertEquals(anElement, stack.pop()), () -> assertTrue(stack.isEmpty()) ); } @Test @DisplayName("returns the element when peeked but remains not empty") void returnElementWhenPeeked(TestInfo testInfo) { assertAll("Return Element When Peeked", () -> assertEquals("returns the element when peeked but remains not empty", testInfo.getDisplayName(), () -> "TestInfo is injected correctly"), () -> assertEquals(anElement, stack.peek()), () -> assertFalse(stack.isEmpty()) ); }
A výsledok, takýchto nested testov
Nové vlastnosti JUnit 5 – Dynamické testy
Dynamické testy sú testy, ktorá sa generujú za behu. Spustiteľné sú prostredníctvom @FunctionalInterface čo znamená, že implementácia dynamického testu môže byť vykonaná prostredníctvom Java 8 ako lambda expression, alebo ako priama referencia metódy. V JUnit 4 sme na takýto druh testov potrebovali frameworky tretích strán, ako napríklad JUnit-QuickCheck. Taký “property test” v JUnit 4 vyzeral potom takto:
@Property(trials = 5) public void testAddition(int number) { System.out.println("Generated number for testAddition: " + number); Calculator calculator = new Calculator(); calculator.add(number); assertEquals(calculator.getResult(), number); }
Ako dynamický test vyzerá v JUnit 5 sa pozrieme cez triedu MyDynamicTest.java
import com.example.Calculator; import org.junit.jupiter.api.*; import org.junit.jupiter.api.extension.ExtendWith; import java.util.Random; import java.util.stream.IntStream; import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.DynamicTest.dynamicTest; @DisplayName("My Dynamic test class") @ExtendWith(TimingExtension.class) public class MyDynamicTest { Calculator calc; @BeforeEach void initCalc() { calc = new Calculator(); } @TestFactory @DisplayName("Dynamic test method add") Stream<DynamicTest> myDynamicTestAdd(TestReporter testReporter) { Random rand = new Random(); return IntStream.iterate(1, n -> n + rand.nextInt()).limit(10000).mapToObj( n -> dynamicTest("test for random int: " + n, () -> assertTrue( calc.add(n, n+1) == n+ n + 1)) ); } @TestFactory @DisplayName("Dynamic test method substraction") Stream<DynamicTest> myDynamicTestSubst(TestReporter testReporter) { Random rand = new Random(); return IntStream.iterate(1, n -> n + rand.nextInt()).limit(10000).mapToObj( n -> dynamicTest("test for random int: " + n, () -> assertTrue( calc.subtraction(n, n+1) == n- (n + 1))) ); } }
A jeho výsledok takto: