JUnit 5 prichádza, ste pripravení?

198

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:

 

Dobrý článok? Chceš dostávať ďalšie?

Už viac ako 4 200 z vás dostáva správy e-mailom. Nemusíš sa báť, nie každé ráno. Len občasne.

Tvoj email neposkytneme 3tím stranám. Posielame naňho len informácie z robime.it. Kedykoľvek sa môžete odhlásiť.