poznáme.it Testovanie JUnit 5 prichádza, ste pripravení?

JUnit 5 prichádza, ste pripravení?

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([email protected](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 6 200 ITečkárov dostáva správy e-mailom. Nemusíš sa báť, nie každé ráno. Len občasne.



Súhlasím so spracovaním mojich osobných údajov. ( Viac informácií. )

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

React FE Developer

Prejdi s novým projektom na remote! Ovládaš React a obzeráš sa po novom projekte? Pre medzinárodnú IT spoločnosť hľadáme...

Technical Writer / REMOTE

Si Technical Writer a hľadáš zmysluplný projekt, ktorý bude REMOTE? Teraz máš možnosť pomáhať zlepšovať kvalitu života pacientov prostredníctvom...

Junior/Medior DevOps Specialist / App pre solárne elektrárne

Ak si Junior alebo Medior DevOps Specialist, máš šancu vydať sa na cestu zvyšovania efektivity solárnych elektrární. Pracoval by...

DevOps Architect

Ak si alebo máš ambíciu stať sa DevOps Architect-om, zbystri pozornosť. Máme pre teba projekt!Pre medzinárodnú IT spoločnosť hľadáme...

.NET / Angular Developer

NET/Angular Developer má teraz príležitosť prejsť na REMOTE s novým projektom!Pre spoločnosť, ktorá patrí medzi najvýznamnejších producentov informačných systémov...

DevOps Architect

Ak si alebo máš ambíciu stať sa DevOps Architect-om, zbystri pozornosť. Máme pre teba projekt!Pre medzinárodnú IT spoločnosť hľadáme...

Fronted Developer / Aplikácie pre diabetikov

Páčila by sa Ti zmysluplná práca na produkte - aplikácií, ktorá pomáha ľuďom s tým najcennejším, čo majú -...

Bratislava Game Jam sa tento rok spojil s hernými konferenciami Game Days a Game Developers Session Praha.

Bratislava Game Jam je súťaž programátorov, scenáristov, grafikov, filozofov a ilustrátorov, ktorých...

Ako chutí tá pravá Java? Odpoveď nájdete na JavaDays 2020 ONLINE

Zaujíma vás svet Javy a všetko s ním spojené? Máte záujem načerpať...

Bratislavská coworkingová legenda The Spot ožíva

The Spot – prvý bratislavský coworkingový priestor, v ktorom vznikali a sídlili...

Čítaj ďalej:

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

Už viac ako 6 200 ITečkárov dostáva správy e-mailom. Nemusíš sa báť, nie každé ráno. Len občasne.

Súhlasím so spracovaním mojich osobných údajov. ( Viac informácií. )

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