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: http://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 }<!--?prettify linenums=true?-->
@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