Testy jednostkowe w JUnit & TestNG


Testy jednostkowe w JUnit & TestNG

Testy jednostkowe (z ang. unit tests) weryfikują czy oprogramowanie działa prawidłowo czyli zgodnie z oczekiwaniami. Jeśli jeden z testów nie zakończy się sukcesem to cała aplikacja nie zostanie uruchomiona. Programista dostarcza danych wejściowych i określa oczekiwany wynik. To czy testy należy pisać czy nie myślę, że nikogo nie trzeba przekonywać, brak testów to m.in. brak możliwości refaktoryzacji kodu! Jakie są różnice w testowaniu z użyciem JUnit4 i TestNg? Zapraszam do lektury!

Od wersji JUnit 4 jest możliwość skorzystania ze znanych nam z Javy 1.5 adnotacji – w JUnit 3 trzeba było przestrzegać nazewnictwa metod testowych. Najnowsza wersja JUnit 5 wprowadziła dużo zmian, względem poprzednich wersji – framework został podzielony na trzy niezależne komponenty i wymaga do funkcjonowania Javy w wersji 8:

  • JUnit Platform – platforma do uruchamiania testów (jeśli uruchamiamy testy w IDE, korzystamy z tego komponentu),
  • JUnit Jupiter – API używane do pisania testów,
  • JUnit Vintage – API pozwalające na uruchamianie testów napisanych w starszych wersjach JUnit.

Którą wersję wybrać? Jak to w IT odpowiedź jest prosta – to zależy 🙂 Jeśli projekt nie korzysta z Javy 8 to nie ma możliwości wykorzystania JUnit 5 – lepsze wsparcie w porównaniu do JUnit 4.

TestNG to rozwinięcie biblioteki JUnit. Framework TestNG istniał już kiedy JUnit było w wersji 3. Daje większe możliwości np. uruchamianie testów z użyciem plików XML, grupowanie testów dzięki uzupełnieniu adnotacji @Test (w wersji JUnit 5 możliwe jest to z użyciem adnotacji @Tag), wsparcie dla wielowątkowości, testy zależne od innych metod testujących czy wygodne raportowanie do plików HTML lub XML . Co więcej TestNG umożliwia testowanie aplikacji z użyciem testów funkcjonalnych, akceptacyjnych, integracyjnych itp.

Niezbędne zależności do Mavena pozwalające wykorzystać potencjał JUnit5:

<?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>edu.session</groupId>
    <artifactId>jdbc</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.2</junit.jupiter.version>
        <junit.vintage.version>4.12.2</junit.vintage.version>
        <junit.platform.version>1.0.0-M4</junit.platform.version>
        <logback.version>1.2.3</logback.version>
    </properties>

    <dependencies>

        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>1.4.199</version>
        </dependency>

        <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.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>${junit.jupiter.version}</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.junit.platform</groupId>
            <artifactId>junit-platform-runner</artifactId>
            <version>1.0.2</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>${logback.version}</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>

            <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>

</project>

Przykładowy test:

public class ClassToBeTested {
    public void multiplyTest(int a, int b) {
        return a * b;
    }
}
import org.junit.platform.runner.JUnitPlatform;
import org.junit.runner.RunWith;
import org.junit.jupiter.api.Test;
import org.junit.platform.suite.api.IncludeTags;
import org.junit.jupiter.api.Tag;

import static org.junit.jupiter.api.Assertions.assertEquals;

@RunWith(JUnitPlatform.class)
@IncludeTags("production")
public class MyTests {

    @Test
    @Tag("development")
    public void multiplyDevelopmentTest() {
        System.out.println("run test 1");
        assertEquals(6, 6);
    }

    @Test
    @Tag("production")
    public void multiplyProductionTest() {
        System.out.println("run test 2");
        assertEquals(6, 6);
    }
}

Uruchomienie testów i przedstawienie wyniku na konsoli:

import org.junit.runner.JUnitCore;
import org.junit.runner.Result;
import org.junit.runner.notification.Failure;

public class MyTestRunner {
  public static void main(String[] args) {
    Result result = JUnitCore.runClasses(ClassToBeTested.class);
    for (Failure failure : result.getFailures()) {
      System.out.println(failure.toString());
    }
  }
}

W przypadku Mavena aby była możliwość wyboru grupy uruchamianych testów w zależności od wybranego taga należy dodać do konfiguracji pliku pom.xml np. poniżej zamieszczony profil:

  <profiles>
        <profile>
            <id>production test</id>
            <properties>
                <tests>production</tests>
            </properties>
        </profile>
    </profiles>

oraz do konfiguracji pluginu maven-surefire-plugin:

<properties>
    <includeTags>${tests}</includeTags>
</properties>

Test parametryczny uruchamiany dla przykładu 3 razy dla podanych danych wejściowych. [UWAGA] występuje problem z uruchamianiem testów parametrycznych z użyciem tagów – więcej tutaj: https://github.com/serenity-bdd/serenity-core/issues/1378

import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;

import java.util.Arrays;
import java.util.Collection;

import static org.junit.Assert.assertEquals;
import static org.junit.runners.Parameterized.*;

@RunWith(Parameterized.class)
public class ParameterizedTestFields {

    @Parameter(0)
    public int m1;
    @Parameter(1)
    public int m2;
    @Parameter(2)
    public int result;
    @Parameters
    public static Collection<Object[]> data() {
        Object[][] data = new Object[][] { { 4, 2, 2 }, { 5, 3, 15 }, { 6, 3, 2 } };
        return Arrays.asList(data);
    }

    @Test
    public void testMultiplyException() {
        ClassToBeTested classToBeTested = new ClassToBeTested ();
        assertEquals("Result", result, classToBeTested.multiply(m1, m2));
    }
}

Niezbędne zależności do Mavena pozwalające wykorzystać potencjał TestNG:

<dependency>
    <groupId>org.testng</groupId>
    <artifactId>testng</artifactId>
    <version>6.14.3</version>
    <scope>test</scope>
</dependency>

Klasa zawierająca testy, każdy test należy do pewnej grupy testów:

import org.testng.annotations.Test;

public class RegularExpressionGroupTest {

    @Test(groups = { "include-test-one" })
    public void testMethodOne() {
        System.out.println("Test method one");
    }

    @Test(groups = { "include-test-two" })
    public void testMethodTwo() {
        System.out.println("Test method two");
    }

    @Test(groups = { "test-one-exclude" })
    public void testMethodThree() {
        System.out.println("Test method three");
    }

    @Test(groups = { "test-two-exclude" })
    public void testMethodFour() {
        System.out.println("Test method Four");
    }
}

W katalogu src/test/resources tworzymy plik testng.xml (nazwa pliku dowolna, nie ma ograniczeń):

<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" >

<suite name="Group of group Suite" verbose="1">
    <test name="Group of group Test">
        <groups>
            <define name="include-group">
                <include name="include-test-one" />
                <include name="include-test-two" />
            </define>
            <define name="exclude-group">
                <include name="test-one-exclude" />
                <include name="test-two-exclude" />
            </define>
            <run>
                <include name="include-group" />
                <exclude name="exclude-group" />
            </run>
        </groups>
        <classes>
            <class name="pl.testng.RegularExpressionGroupTest" />
        </classes>
    </test>
</suite>

W wyżej opisanej konfiguracji dwa testy zostaną wykonane a dwa wyłączone z użycia.

Plugin surefire:

<plugin>
	<groupId>org.apache.maven.plugins</groupId>
	<artifactId>maven-surefire-plugin</artifactId>
	<version>2.14.1</version>
	<configuration>
		<suiteXmlFiles>
			<suiteXmlFile>src\test\resources\testng.xml</suiteXmlFile>
		</suiteXmlFiles>
	</configuration>
</plugin>

Test uruchamiany w 3 niezależnych wątkach 6 razy z wyznaczonym maksymalnym czasem wykonania przypadku testowego – 1000ms:

public class ThreadTest {
    @Test(threadPoolSize = 3, invocationCount = 6, timeOut = 1000)
    public void testMethod() {
        Long id = Thread.currentThread().getId();
        System.out.println("Test method executing on thread with id: " + id);
    }
}

Testy zależne od innych testów:

import org.testng.annotations.Test;

public class App {

    @Test
    public void AppMethod1() {
        System.out.println("App method - 1");
        throw new RuntimeException();
    }

    @Test(dependsOnMethods = { "AppMethod1" })
    public void AppMethod2() {
        System.out.println("App method - 2");
    }
}

Drugi test zostanie zignorowany:

App method - 1

java.lang.RuntimeException
	at pl.testng.App.AppMethod1(App.java:10)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	...
Test ignored.

Raportowanie z użyciem domyślnego mechanizmu biblioteki TestNG, mimo możliwości raportowania wyników testów w formacie HTML nie jest zbyt intuicyjne. Alternatywą jest projekt ReportNG:

Uzupełnienie konfiguracji pluginu surefire:

<properties>
	<property>
		<name>usedefaultlisteners</name>
		<value>false</value>
	</property>
	<property>
		<name>listener</name>
		<value>org.uncommons.reportng.HTMLReporter, org.uncommons.reportng.JUnitXMLReporter</value>
	</property>
</properties>

<workingDirectory>target/ReportNg</workingDirectory>

Niezbędne zależności:

 <!-- java.lang.NoClassDefFoundError: com/google/inject/Injector -->
<dependency>
	<groupId>com.google.inject</groupId>
	<artifactId>guice</artifactId>
	<version>3.0</version>
	<scope>test</scope>
</dependency>

<dependency>
	<groupId>org.uncommons</groupId>
	<artifactId>reportng</artifactId>
	<version>1.1.2</version>
	<scope>test</scope>
	<exclusions>
		<exclusion>
			<groupId>org.testng</groupId>
			<artifactId>testng</artifactId>
		</exclusion>
	</exclusions>
</dependency>

Repozytorium – (biblioteka ReportNG nie jest dostępna w centralnym repozytorium Mavena):

<!-- Unfortunately ReportNG jar isn’t available in Maven Central Repository -->
<repositories>
	<repository>
		<id>java-net</id>
		<url>http://download.java.net/maven/2</url>
	</repository>
</repositories>

Przykładowy test integracyjny z użyciem TestNG i Spring Boot:

public class Employee {

    private String empId;
    private String name;
    private String designation;
    private double salary;

   // getters & setters
}
@RestController
public class TestController {

    @RequestMapping(value = "/employee", method = RequestMethod.GET)
    public Employee firstPage() {

        Employee emp = new Employee();
        emp.setName("emp1");
        emp.setDesignation("manager");
        emp.setEmpId("1");
        emp.setSalary(3000);

        return emp;
    }
}
import org.springframework.test.context.testng.AbstractTestNGSpringContextTests;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest(classes = TestNgIntegrationTestApplication.class)
public class SpringBootHelloWorldTests extends AbstractTestNGSpringContextTests {

    @Autowired
    private WebApplicationContext webApplicationContext;

    private MockMvc mockMvc;

    @BeforeClass
    public void setup() {
        mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
    }

    @Test
    public void testEmployee() throws Exception {
        mockMvc.perform(get("/employee")).andExpect(status().isOk())
                .andExpect(content().contentType("application/json;charset=UTF-8"))
                .andExpect(jsonPath("$.name").value("emp1")).andExpect(jsonPath("$.designation").value("manager"))
                .andExpect(jsonPath("$.empId").value("1")).andExpect(jsonPath("$.salary").value(3000));

    }

}


Leave a comment

Your email address will not be published.


*