Nowe możliwości JUnit 4
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
- Testy sparametryzowane
- Spodziewany wyjątek
- Testowanie wydajności
- Ignorowanie testu
- Połączenie testów w zestaw
- Podsumowanie
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).
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 a i b. 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, sum i dif 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=4 i 2-2=0.
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.
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.
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.
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 {
}
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.



