JaCoCo, JUnit + Maven - Badanie Pokrycia Testami

Pisanie dobrego kodu wymaga od programisty równoległego pisania testów, istotne jest aby testy pokrywały jak najwięcej tworzonego kodu. Nie trudno, podczas pisania testów pominąć fragment kodu, co w przyszłości może przysporzyć sporo problemów i być powodem frustracji programisty. Na szczęście z pomocą przychodzą narzędzia do badania pokrycia kodu testami takie jak JaCoCo, którego zastosowanie chciałbym dzisiaj zaprezentować.

Przedmiotem dzisiejszego wpisu jest integracja narzędzia JaCoCo z Maven oraz rozdzielenie testów jednostkowych od integracyjnych. Dlaczego rozdzielenie testów jednostkowych i integracyjnych jest takie ważne? Głównym argumentem przemawiającym na korzyść rozdzielenia testów jest czas potrzebny na ich przeprowadzenie. Testy jednostkowe z reguły trwają krótko i są stosowane do testowania poszczególnych metod lub fragmentów kodu. Testy integracyjne natomiast badają złożone procesy, przez co czas ich wykonania ulega znacznemu wydłużeniu.

Tyle słowem wstępu, czas przejść do praktycznego zastosowania.

Tworzenie projektu

Zaczynajmy! Na początek utworzę nowy projekt za pomocą Maven`a. Osobiście jestem zwolennikiem wiersza poleceń, więc utworzę projekt za pomocą poniższego polecenia i zaimportuje go do Eclipse. Moje podejście to kwestia indywidualnych upodobań, także nic nie stoi na przeszkodzie, aby projekt utworzyć za pomocą dowolnego IDE.

mvn archetype:generate -DgroupId=pl.gruberski -DartifactId=jacoco-code-coverage -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false

Dodatkowy katalog dla testów integracyjnych

Aby projekt był bardziej czytelny i lepiej uporządkowany, proponuje fizycznie rozdzielić testy jednostkowe od integracyjnych. Często w przykładach testy rozdzielane są na podstawie nazwy klasy lub implementowanego interfejsu. Uważam jednak, że fizyczne rozmieszczenie testów w osobnych folderach znacznie poprawia porządek i ułatwia pracę z projektem.

Rozdzielenie można zrealizować za pomocą pluginu do Maven`a Build Helper Maven Plugin, dzięki któremu możliwe jest dodanie dodatkowych katalogów do projektu. Moje testy integracyjne trafią to katalogu “src/integration-test/java”. Konfiguracja pluginu ogranicza się do dodania następującego wpisu do pliku pom.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<build>
    <plugins>
    ...
    <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>build-helper-maven-plugin</artifactId>
        <version>1.9</version>
        <executions>
            <execution>
                <id>add-integration-test-source</id>
                <phase>generate-test-sources</phase>
                <goals>
                    <goal>add-test-source</goal>
                </goals>
                <configuration>
                    <sources>
                        <source>src/integration-test/java</source>
                    </sources>
                </configuration>
            </execution>
        </executions>
    </plugin>
    ...
    </plugins>
</build>

Dodawanie pluginu JaCoCo do konfiguracji Maven`a

Integrację rozpoczynamy od dodania pluginu JaCoCo Maven Plugin do konfiguracji Maven`a. W tym celu należy dodać poniższy wpis do pliku pom.xml:

1
2
3
4
5
6
7
8
9
10
11
<build>
    <plugins>
    ...
    <plugin>
        <groupId>org.jacoco</groupId>
        <artifactId>jacoco-maven-plugin</artifactId>
        <version>0.7.1.201405082137</version>
    </plugin>
    ...
    </plugins>
</build>

Konfiguracja raportów dla testów jednostkowych i integracyjnych

Plugin JaCoCo należy skonfigurować, aby generował dwa rodzaje raportów, osobny dla testów jednostkowych i osobny dla testów integracyjnych. Generowanie raportu odbywa się w dwóch etapach. Pierwszy etap zbiera dane na temat pokrycia testami i zapisuje wyniki do pliku, drugi generuje raport i zapisuje go w kilku ustandaryzowanych formatach (html, csv, xml).

Konfiguracje należy dodać do sekcji executions pluginu JaCoCo Maven Plugin w pliku pom.xml, po dodaniu konfiguracji wpis będzie wyglądał następująco:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>0.7.1.201405082137</version>
    <executions>
        <execution>
            <id>init-prepare-agent</id>
            <goals>
                <goal>prepare-agent</goal>
            </goals>
            <configuration>
                <destFile>${project.build.directory}/jacoco-cc/jacoco-ut.exec</destFile>
                <propertyName>surefireArgLine</propertyName>
            </configuration>
        </execution>
        <execution>
            <id>unit-report</id>
            <phase>test</phase>
            <goals>
                <goal>report</goal>
            </goals>
            <configuration>
                <dataFile>${project.build.directory}/jacoco-cc/jacoco-ut.exec</dataFile>
                <outputDirectory>${project.reporting.outputDirectory}/jacoco-ut</outputDirectory>
            </configuration>
        </execution>
        <execution>
            <id>integration-prepare-agent</id>
            <goals>
                <goal>prepare-agent-integration</goal>
            </goals>
            <configuration>
                <destFile>${project.build.directory}/jacoco-cc/jacoco-it.exec</destFile>
                <propertyName>failsafeArgLine</propertyName>
            </configuration>
        </execution>
        <execution>
            <id>integration-report</id>
            <phase>verify</phase>
            <goals>
                <goal>report-integration</goal>
            </goals>
            <configuration>
                <dataFile>${project.build.directory}/jacoco-cc/jacoco-it.exec</dataFile>
                <outputDirectory>${project.reporting.outputDirectory}/jacoco-it</outputDirectory>
            </configuration>
        </execution>
    </executions>
</plugin>

Konfiguracja Maven Surefire Plugin

Aby zapewnić generowanie raportów razem z naszymi testami jednostkowymi, należy do konfiguracji pluginu Maven`a odpowiedzialnego za testy jednostkowe (Maven Surefire Plugin) dodać argumenty wiersza poleceń wygenerowane w pluginie JaCoCo Maven Plugin. Argumenty zostały zapisane w zmiennej surefireArgLine. Po zmianach definicja Maven Surefire Plugin w pliku pom.xml powinna wyglądać następująco:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<build>
    <plugins>
    ...
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>2.17</version>
        <configuration>
            <excludes>
                <exclude>**/IT*.java</exclude>
            </excludes>
            <argLine>${surefireArgLine}</argLine>
        </configuration>
    </plugin>
    ...
    </plugins>
</build>

W powyższej deklaracji można zauważyć dodatkowy wpis exclude. Ma on na celu odłączenie testów integracyjnych, które nie mają być uruchamiane razem z testami jednostkowymi. Wpis ten wymaga, aby wszystkie testy integracyjne były deklarowane w plikach o nazwie zaczynającej się od IT, przykładowo ITExample.java.

Konfiguracja Maven Failsafe Plugin

Konfiguracja Maven Failsafe Plugin, odpowiedzialnego za uruchamianie testów integracyjnych jest zbliżona do wcześniej wykonanej konfiguracji testów jednostkowych. Należy do konfiguracji Maven Failsafe Plugin dodać argumenty wiersza poleceń wygenerowane w pluginie JaCoCo Maven Plugin. Argumenty zostały zapisane w zmiennej failsafeArgLine. Po zmianach definicja Maven Failsafe Plugin w pliku pom.xml powinna wyglądać następująco:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<build>
    <plugins>
    ...
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-failsafe-plugin</artifactId>
        <version>2.17</version>
        <executions>
            <execution>
                <goals>
                    <goal>integration-test</goal>
                    <goal>verify</goal>
                </goals>
                <configuration>
                    <argLine>${failsafeArgLine}</argLine>
                </configuration>
            </execution>
        </executions>
    </plugin>
    ...
    </plugins>
</build>

Przykładowe testy

Gdy mamy wykonaną konfigurację JaCoCo, możemy przejść do pisania testów. Na potrzeby przykładu utworzę prostą klasę z jedną metodą, którą będziemy testować. Metoda będzie zwracała odpowiedni łańcuch znaków w zależności od podanego parametru. Jeżeli parametr będzie inny od oczekiwanego metoda rzuci wyjątek IllegalArgumentException.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package pl.gruberski;

public class Foobar {

    public String getWord(int number) {

        if(number == 1) {
            return "Foo";
        }
        else if(number == 2) {
            return "Bar";
        }
        else {
            throw new IllegalArgumentException("invalid argument");
        }
    }
}

Dla powyższej klasy tworzę podstawowe testy jednostkowe z wykorzystaniem JUnit. Pomijam sprawdzanie wyjątku rzucanego przez metodę, aby lepiej zobrazować wyniki uzyskane w raporcie pokrycia kodu testami.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package pl.gruberski;

import static org.junit.Assert.*;

import org.junit.Before;
import org.junit.Test;

public class FoobarTest {

    private Foobar foobar;

    @Before
    public void setUp() {
      foobar = new Foobar();
    }

    @Test
    public void getWordIfNumberIsOne() {
      assertEquals(foobar.getWord(1), "Foo");
    }

    @Test
    public void getWordIfNumberIsTwo() {
      assertEquals(foobar.getWord(2), "Bar");
    }
}

Tworzę także podstawowe testy integracyjne (na potrzeby przykładu wykorzystuje te same testy co przy testach jednostkowych). Należy pamiętać, aby plik z testami integracyjnymi miał odpowiednią nazwę pliku. Zgodnie z wcześniejszymi deklaracjami w konfiguracji, pliki z testami integracyjnymi muszą posiadać nazwę rozpoczynającą się od IT (np. ITExample.java).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package pl.gruberski;

import static org.junit.Assert.assertEquals;

import org.junit.Before;
import org.junit.Test;

public class ITFoobarTest {

private Foobar foobar;

    @Before
    public void setUp() {
      foobar = new Foobar();
    }

    @Test
    public void getWordIfNumberIsOne() {
      assertEquals(foobar.getWord(1), "Foo");
    }

    @Test
    public void getWordIfNumberIsTwo() {
     assertEquals(foobar.getWord(2), "Bar");
    }
}

Uruchomienie testów

Gdy mamy już gotowe podstawowe testy, możemy przejść do ich uruchomienia. Wraz z testami wygenerowane zostaną raporty pokrycia kodu testami.

mvn clean test # uruchamianie testów jednostkowych wraz z generowaniem raportu
mvn clean verify # uruchamianie testów jednostkowych i integracyjnych wraz z generowaniem raportu

Wyniki testów i raport pokrycia kodu testami

Jeżeli konfiguracja została wykonana poprawnie i testy przebiegły pomyślnie, w katalogu target/site projektu powinny być dostępne raporty pokrycia kodu testami. Raporty dla testów jednostkowych i integracyjnych będą znajdowały się w osobnych folderach, których nazwy podaliśmy przy konfiguracji JaCoCo Maven Plugin.

Jeżeli chcemy zapoznać się ze szczegółowym pokryciem kodu testami dla poszczególnych klas, wystarczy przeklikać strukturę projektu zaczynając od głównego pakietu (w tym przypadku pl.gruberski).


Powyższy obrazek pokazuje, że kod klasy został w dużym stopniu pokryty testami. Brakuje jednak testu dla przypadku, w którym metoda rzuci wyjątek IllegalArgumentException.

Dodatkowe możliwości

Jeżeli chcielibyśmy mieć możliwość uruchamiania tylko testów integracyjnych (z pominięciem testów jednostkowych), możemy zastosować profile kompilacji Maven`a, które pozwalają na indywidualną konfigurację projektu w zależności od wybranego profilu. Być może opiszę to zagadnienie w niedalekiej przyszłości. Proszę o informację w komentarzu, jeżeli dla kogoś jest to temat interesujący i warty opisania.

Narzędzie JaCoCo oprócz dostarczania surowego raportu na temat pokrycia kodu testami, idealnie nadaje się do integracji z serwerami Continuous Integration. Planuje w jednym z kolejnych wpisów przyjrzeć się dokładniej serwerowi Jenkins CI oraz integracji go z wieloma ciekawymi rozwiązaniami (np. JaCoCo, Checkstyle itp.).

Podsumowanie

To by było na tyle. Zapraszam do dyskusji w komentarzach, wszelkie uwagi oraz sugestie na temat wpisu (lub bloga) są mile widziane.
Jest to mój pierwszy merytoryczny wpis, więc z góry przepraszam za błędy lub niejasne opisy. Postaram się, aby z każdym kolejnym wpisem było tylko lepiej.

Cały projekt znajduje się w rapozytorium pod adresem https://github.com/gruberski/jacoco-code-coverage.

Comments