Kwiecień
10
2009

Nowe możliwości JUnit 4

Słowa kluczowe: , , | Kategorie: Java
No Gravatar

Biblioteka JUnit zawsze była potężnym narzędziem. Jednak wersja 4 pozwala w znacznym stopniu przyspieszyć tworzenie znacznie bardziej złożonych testów. Dzisiaj spróbuję omówić parę najważniejszych usprawnień.

Spis treści

Testowana klasa

Na początek utworzę interfejs – niech to będzie kalkulator:

public interface Calc {
    void clear();
    long get();
    void add(long number);
    void sub(long number);
    void mul(long number);
    void div(long number);
    void power(byte number);
}

Metody interfejsu to: wyzerowanie pamięci (clear), pobranie wartości (get), dodawanie (add), odejmowanie (sub), mnożenie (mul), dzielenie (div) i potęgowanie (power). Utworzyłem też prostą implementację SimpleCalc, którą pominę ze względu na prostotę.

Czytelnikowi polecam utworzenie własnej implementacji interfejsu Calc po napisaniu testów – w taki sposób, aby testy zakończyły się sukcesem – programowanie sterowane testami (ang. TDD).

Testy sparametryzowane

Dla dodawania i utworzyłem sparametryzowaną klasę testową (@RunWith(Parameterized.class)):

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

import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;

import static org.junit.Assert.*;

@RunWith(Parameterized.class)
public class SimpleCalcTestAddSub {
}

Co oznacza ta parametryzacja, wyjaśnię w dalszej części. Najpierw dodam statyczny kalkulator, który tworzony będzie przed wywołaniem testów tej klasy, a usuwany po ich wywołaniu:

private static Calc calc = null;

@BeforeClass
public static void setUpBeforeClass()
throws Exception {
    calc = new SimpleCalc();
}

@AfterClass
public static void tearDownAfterClass()
throws Exception {
    calc = null;
}

Testy będą działać na liczbach ab. Wyniki będą porównywane ze spodziawaną sumą (sum) albo różnicą (dif):

private long a;
private long b;
private long sum;
private long dif;

Przed każdą metodą testową wartość kalkulatora będzie ustawiana na a, a po wywołaniu każdej metody będzie zerowana:

@Before
public void setUp() throws Exception {
    calc.add(a);
}

@After
public void tearDown() throws Exception {
    calc.clear();
}

Testy opierają się o wywołanie odpowiedniej operacji oraz sprawdzenie wyniku (najpierw wartość spodziewana, potem uzyskany wynik!):

@Test
public void testAdd() {
    calc.add(b);
    assertEquals("The sum is wrong",
            sum, calc.get());
}

@Test
public void testSub() {
    calc.sub(b);
    assertEquals("The difference is wrong",
            dif, calc.get());
}

Wartości a, b, sumdif ustawiane będą w konstruktorze:

public SimpleCalcTestAddSub(
        long a, long b, long sum, long dif) {
    this.a = a;
    this.b = b;
    this.sum = sum;
    this.dif = dif;
}

Jednak konstruktor wymaga parametrów. Tutaj pojawia się statyczna metoda, zwracająca parametry dla sparametryzowanej klasy testowej:

@Parameters
public static Collection<Object[]> getParameters() {
    return Arrays.asList(new Object[][] {
            {2, 2, 4, 0},
            {3, 2, 5, 1},
            {2, 3, 5, -1},
            {17, 35, 52, -18},
    });
}

Na szczególną uwagę zasługuje fakt, iż generowana jest kolekcja tablic parametrów – konstruktor oraz wszystkie metody testowe zostaną wywołane dla każdego zestawu parametrów. Na przykład dla pierwszego wiersza (↑ linijka 4) będzie to: SimpleCalcTestAddSub(2, 2, 4, 0), 2+2=42-2=0.

Spodziewany wyjątek

Kolejną klasę utworzę dla dzielenia i potęgowania:

import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

import static org.junit.Assert.*;

public class SimpleCalcTestDivPower {
    private static Calc calc = null;

    @BeforeClass
    public static void setUpBeforeClass() throws Exception {
        calc = new SimpleCalc();
    }

    @AfterClass
    public static void tearDownAfterClass() throws Exception {
        calc = null;
    }

    @Before
    public void setUp() throws Exception {
        calc.clear();
    }
}

Obiekt kalkulatora tworzony jest jak poprzednio, zaś pamięć zerowana jest przed każdą metodą testową.

Najpierw standardowe dzielenie – tu raczej wszystko powinno być jasne:

private static final long a = 100L;
private static final long b = 50L;
private static final long quotient = 2L;

@Test
public void testDivision() {
    calc.add(a);
    calc.div(b);
    assertEquals("The quotient is wrong",
            quotient, calc.get());
}

Ale trzeba przewidzieć też szczególny przypadek – dzielenie przez zero. Teraz sprawdzenie poprawności działania sprowadza się do określenia wyjątku, jaki powinien wystąpić podczas wywołania metody testowej. W tym celu należy zdefiniować atrybut expected adnotacji Test:

private static final long zero = 0L;

@Test(expected = ArithmeticException.class)
public void testDivisionByZero() {
    calc.add(a);
    calc.div(zero);
}

Jeżeli wyjątek nie zostanie zgłoszony, test zakończy się niepowodzeniem z komunikatem: Expected exception: java.lang.ArithmeticException.

Testowanie wydajności

Aby przetestować szybkość działania algorytmu, w klasie SimpleCalc zaimplementowałem przesadnie nieefektywną metodę power:

public void power(byte number) {
    long v = this.value;
    for (byte p = 1; p < number; p++) {
        for (
                long i = 0;
                i < this.value * (v - 1);
                i++) {
            this.value++;
        }
    }
}

Teraz przetestuję czas trwania obliczeń dla wyrażenia: 230. Przy pomocy atrybutu timeout określonego dla adnotacji Test ustawiam maksymalny czas na 10 ms:

private static final long base = 2L;
private static final byte exponent = 30;
private static final long power = 1L << 30;

@Test(timeout = 10)
public void testPower() {
    calc.add(base);
    calc.power(exponent);
    assertEquals("The power is wrong", power, calc.get());
}

Na moim komputerze potęgowanie trwa 15–30 ms, więc test kończy się błędem z komunikatem: test timed out after 10 milliseconds. Nie jest to jakieś olbrzymie opóźnienie, ale dla tysięcy potęgowań obliczenia mogą potrwać całe minuty.

Ignorowanie testu

Ponieważ w tej chwili nie zamierzam przyspieszać algorytmu potęgowania, na razie oznaczę ten test jako ignorowany:

@Ignore("Algorithm is too slow")
@Test(timeout = 10)
public void testPower() { // ...

Podczas uruchomienia testów, metoda wciąż będzie widoczna, lecz tym razem zostanie oznaczona jako ignorowana. Dodatkowo wyniki zostaną opatrzone informacją o ilości ignorowanych testów: Runs: 3/3 (1 ignored) Errors: 0 Failures: 0.

Połączenie testów w zestaw

Na koniec połączę oba testy w jeden zestaw, aby:

  • logicznie połączyć testy dotyczące jednej klasy,
  • wywoływać wszystkie testy jednocześnie.

Cała klasa zestawu testów wygląda następująco:

import org.junit.runner.RunWith;
import org.junit.runners.Suite;

@RunWith(Suite.class)
@Suite.SuiteClasses({
    SimpleCalcTestAddSub.class,
    SimpleCalcTestDivPower.class,
})
public class SimpleCalcTest {
}

Podsumowanie

Funkjonalność biblioteki JUnit została znacznie zwiększona przy jednoczesnym uproszczeniu, dzięki wykorzystaniu możliwości płynących z użycia adnotacji. Jednak to jeszcze nie koniec – następnym razem przeniosę testy na nowy poziom, łącząc JUnit 4 z typami generycznymi.

Napisz Komentarz

*