Mariusz Prowaźnik

o programowaniu w Javie, Scali i Clojure.


Jak rozdzielić testy jednostkowe i integracyjne (z wykorzystaniem JUnit i Maven)

Jakiś czas temu pisałem o użyciu kategorii w testach JUnit. Możliwość oznaczenia testu kategorią jest przydatna, pod warunkiem, że w jakiś sposób Maven, wykonując testy, będzie w stanie je rozróżnić i wykonać tylko testy określonej kategorii, bo jednak ręczne dodawanie testów do zestawów (TestSuite) mija się z celem. Na szczęście taka konfiguracja Maven'a jest to możliwa.

Ok, mamy dwa interfejsy, które będą markerami dla naszych testów:
package com.evojava;
public interface IntegrationTest {}
package com.evojava;
public interface UnitTest {}
Testy niech będą następujące:
package com.evojava;

import org.junit.Test;
import org.junit.experimental.categories.Category;

@Category(UnitTest.class)
public class SmallTest {

 @Test
 public void test() {
  System.out.println("SmallTest");
 }
}
package com.evojava;
import org.junit.Test;
import org.junit.experimental.categories.Category;

@Category(IntegrationTest.class)
public class BigTest {

 @Test
 public void test() {
  System.out.println("BigTest");
 }
}

Za pomocą wtyczki Surefire, możemy skonfigurować projekt tak by uruchamiały się testy oznaczone określoną kategorią. Przykładowy plik pom.xml:
<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>com.evojava</groupId>
 <artifactId>categories</artifactId>
 <version>0.0.1-SNAPSHOT</version>
 <name>categories</name>
 <description>categories</description>
 <dependencies>
  <dependency>
   <groupId>junit</groupId>
   <artifactId>junit</artifactId>
   <version>4.10</version>
  </dependency>
 </dependencies>
 <build>
  <plugins>
   <plugin>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>2.11</version>
    <configuration>
     <groups>com.evojava.UnitTest</groups>
    </configuration>
    <dependencies>
     <dependency>
      <groupId>org.apache.maven.surefire</groupId>
      <artifactId>surefire-junit47</artifactId>
      <version>2.12</version>
     </dependency>
    </dependencies>
   </plugin>
  </plugins>
 </build>
</project>
Wykonujemy: mvn clean package
-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Concurrency config is parallel='none', perCoreThreadCount=true, threadCount=2, useUnlimitedThreads=false
Running com.evojava.SmallTest
SmallTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.002 sec
Results :
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

Teraz wystarczy zmienić konfigurację, by przy wykonaniu mvn integration-test uruchomiły się pozostałe testy. Odpowiedni fragment pliku pom.xml:
<plugin>
 <artifactId>maven-surefire-plugin</artifactId>
 <version>2.11</version>
 <executions>
  <execution>
   <id>default-test</id>
   <phase>test</phase>
   <goals>
    <goal>test</goal>
   </goals>
   <configuration>
    <groups>com.evojava.UnitTest</groups>
   </configuration>
  </execution>
  <execution>
   <id>integration-tests</id>
   <phase>integration-test</phase>
   <goals>
    <goal>test</goal>
   </goals>
   <configuration>
    <excludedGroups>com.evojava.UnitTest</excludedGroups>
   </configuration>
  </execution>
 </executions>
 <dependencies>
  <dependency>
   <groupId>org.apache.maven.surefire</groupId>
   <artifactId>surefire-junit47</artifactId>
   <version>2.12</version>
  </dependency>
 </dependencies>
</plugin>
Wtedy, po uruchomieniu mvn clean integration-test
-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Concurrency config is parallel='none', perCoreThreadCount=true, threadCount=2, useUnlimitedThreads=false
Running com.evojava.BigTest
BigTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.002 sec

Results :
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
Na koniec krótkie wyjaśnienie do tej konfiguracji.
  • Zostały skonfigurowane dwa uruchomienia testów: <id>default-test</id>, <id>integration-tests</id>.

  • default-test został przypięty do fazy test, integration-test do fazy integration-test.

  • W domyślnym cyklu budowania są między innymi fazy: validate, compile, test, package, integration-test, verify, install, deploy. Po mvn test będą uruchomione fazy do test włącznie, po mvn integration-test do integration-test włącznie (czyli po drodze również fazę test). Więcej na ten temat

  • default-test uruchamia tylko testy oznaczone kategorią UnitTest, integration-test wszystkie oprócz oznaczonych kategorią UnitTest. W rezultacie mvn test uruchamia testy jednostkowe, mvn integration-test wszystkie testy (bo faza test również się wykona)

  • Nazwa default-test jest istotna, bo jeśli nie skonfigurujemy jawnie tego uruchomienia, uruchomi domyślnie wszystkie testy

  • Jeśli w integration-test mają uruchomić się tylko testy oznaczone kategorią IntegrationTest, a nie wszystkie oprócz UnitTest, wystarczy zmienić:
    <configuration>
      <excludedGroups>com.evojava.UnitTest</excludedGroups>
    </configuration>
    na:
    <configuration>
      <groups>com.evojava.IntegrationTest</groups>
    </configuration>
    Akurat w tym przypadku na jedno wychodzi, ale jeśli pojawią kolejne kategorie, to już będzie różnica.
Kod można ściągnąć z https://evo-java.googlecode.com/svn/maven-int-test-example/

4 komentarze :

  1. Bardzo fajny wpis. Czegos takiego szukalem. Czy moglbys go rozwinac, tak zeby opisac co zrobic, zeby mvn integration-test urchomil nie tylko BigTest, ale i rowniez SmallTest (i byc moze MediumTest, jezeli takie powstana w przyszlosci)?

    OdpowiedzUsuń
  2. Cieszę się, że Ci się przydało. Wyedytowałem post według Twojej sugestii i dodałem kilka wyjaśnień.

    OdpowiedzUsuń
  3. Pomocny opis mam jednak pytanie jeżeli projekt składa się z kilku modułów podrzędnych przy korzystaniu z tej metody otrzymuję błąd "There was an error in the forked process"

    OdpowiedzUsuń
  4. Na http://evo-java.googlecode.com/svn/category-int-multimodule/ wrzuciłem przykład projektu składającego się z kilku modułów i używającego kategorie JUnit. Jest projekt:
    -category-int-multimodule
    \-- category
    \-- submodule
    Interfejsy do kategorii są w module category. W pomie category-int-multimodule jest dodana zależność do category. Ważne jest, żeby pom w category nie dziedziczył po category-int-module (nie może być [...]category-int-multimodule), bo będzie cykliczna zależność.

    Możesz spróbować innego sposobu, np użyć konwencji nazewniczej *Test.java dla testów jednostkowych, *IntTest.java dla integracyjnych i rozdzielić je za pomocą wzorców zamiast kategorii:
    http://maven.apache.org/surefire/maven-surefire-plugin/examples/inclusion-exclusion.html

    OdpowiedzUsuń