Programowanie średnio-zaawansowane #21: testy jednostkowe

Znasz już paradygmaty programowania obiektowego, umiesz korzystać z kolekcji oraz obsługiwać wyjątki. Tym razem postaram się wytłumaczyć Ci, jak testować swój kod. Zanim przejdę do omawiania konkretnej implementacji, przypomnij sobie w jaki sposób testowałem swój kod we wszystkich poprzednich wpisach. Tak, słusznie zauważyłeś/aś, że odbywało się to przez użycie metody main. Przyznasz jednak, że nie jest to do końca wygodne rozwiązanie, biorąc choćby pod uwagę, że musisz odpalać taki main dla każdej implementacji oddzielnie. Dodatkowo taki test manualny jest mało miarodajny. Dlatego właśnie Kent Beck, jeden z najbardziej znanych programistów Java, napisał kiedyś* dodatkową bibliotekę o nazwę JUnit. Warto tu podkreślić, że nie jest to narzędzie, które zawiera się w standardzie Javy. Aby używać tego frameworku** musisz go dodatkowo dołączyć do Twojego projektu. Musisz zrozumieć, że idea programowania w Javie polega na tym, aby nie wymyślać koła od nowa, co oznacza, że praktycznie na każdy powszechnie znany problem, istnieje lekarstwo w postaci dodatkowych rozszerzeń JDK, które powszechnie nazywa się frameworkami.

Zanim napiszę swój pierwszy test, zaimplementuję wpierw prostą klasę z czterema metodami.

public class MyCalculator {

	public int add(Integer numberOne, Integer numberTwo) {
		return numberOne + numberTwo;
	}

	private int subtract(Integer numberOne, Integer numberTwo) {
		return numberOne - numberTwo;
	}

	protected int multiply(Integer numberOne, Integer numberTwo) {
		return numberOne * numberTwo;
	}

	public double divide(Integer numberOne, Integer numberTwo) {
		return (double) numberOne / numberTwo;
	}
}

Jak widzisz, nie ma tu jakiś skomplikowanych rzeczy. Zwykła klasa wykonująca dodawanie, odejmowanie, mnożenie i dzielenie. Zwróć jednak uwagę, że przed każdą z nich wstawiłem zupełnie inny modyfikator dostępu. Zrobiłem to umyślnie, żeby pokazać Ci, że w teście jesteś wstanie przetestować nie tylko metody publiczne, ale także chronione i domyślne. Oczywiście metoda typu prywatnego nie jest testowalna.

Teraz należy dołączyć bibliotekę JUnit do Twojego projektu. W zależności jakiego IDE używasz, kroki do osiągnięcia tego będą inne. Tutaj skupię się tylko na pokazaniu jak taką operację należy wykonać używając Eclipse’a.

W skrócie wygląda to tak:

  1. Użyj prawego przycisku myszy na nazwie swojego projektu.
Menu kontekstowe Eclipse’a

2. Następnie klikasz na Properties i w okienku Java Build Path na zakładkę Libraries.

Okienko do wstawiania dodatkowych bibliotek

3. Tutaj możesz zobaczyć swoją wersje JDK. Teraz dodam bibliotekę JUnit do mojego projektu poprzez kliknięcie Add Library… a następnie Junit i wersję (w moim przypadku będzie to wersja 4).

Klikasz na Next, ustawiasz wersję na 4 i Finish

4. Teraz w okienku Java Build Path powinien znajdować też JUnit 4.

Super. To teraz poniżej pokażę Ci przykładowy rozkład metod w moim teście jednostkowym.

import static org.junit.Assert.*;

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

public class MyCalculatorTest {

	@BeforeClass
	public static void setUpBeforeClass() throws Exception {
	}

	@Before
	public void setUp() throws Exception {
	}

	@Test
	public void testAdd() {
		fail("Not yet implemented");
	}

	@Test
	public void testMultiply() {
		fail("Not yet implemented");
	}

	@Test
	public void testDivide() {
		fail("Not yet implemented");
	}
}

Ponieważ w klasie, którą testuję jedna metoda jest prywatna, to jak już mówiłem, nie mogę ją przetestować. Bardzo ważna kwestia jest zrozumienie, że testy jednostkowe należy trzymać w tym samym pakiecie co klasy zawierające implementacje. To wcale nie znaczy, że zawsze będą one w tym samym katalogu, bowiem Java umożliwia trzymanie takich samych pakietów w różnych ścieżkach***. W tym jednak przypadku dla uproszczenia stwórz klasę testująca w tym samym pakiecie/ścieżce, gdzie trzymasz swój kod.

Kolejną ważną kwestią jest użycie odpowiednich importów. Ponieważ korzystać w swoim projekcie z zewnętrznej biblioteki JUnit, Twoje IDE powinno bez problemu znaleźć odpowiednie pakiety frameworka, tak jak w przykładzie wyżej. Jeśli jednak Eclipse zaznacza Ci je na czerwono, to znaczy, że coś jest nie tak w ustawieniach Twojego projektu.

Następnie zauważ, że skorzystałem w swoim templacie z trzech adnotacji: @Test, @Before i @BeforeClass. Adnotacja Test została ustawiona na każdej z metod. Pamiętaj, że celem testu jednostkowego jest, jak sama nazwa wskazuje, przetestowanie tylko jednej funkcjonalności w kodzie. To znaczy, że powinieneś mieć w klasie testującej przynajmniej jeden test na metodę nieprywatną.

Adnotacją BeforeClass wykonuje jakiś kod przed odpaleniem wszystkich testów w klasie. Z reguły w metodzie ustawiasz jakieś połączenie z zewnętrznym źródłem danych (np. bazą danych) albo po prostu stałe, które będziesz używać w kodzie. Zauważ, że metoda oznaczona tą adnotacją, jest statyczna. Także staraj się unikać inicjacji klas instancyjnych w tej sekcji. Na tym etapie chyba nie muszę mówić, że jest to bez sensu. Jeśli chcesz stworzyć obiekt klasy, który będziesz używać w swoim teście, to zrób to sekcji Before. Adnotacja ta oznacza, że metoda tak ustawiona, będzie wykonywana przed każdym testem oddzielnie.

Przykład mockowania**** danych w teście jednostkowym.

public class MyCalculatorTest {
	private static int numberOne;
	private static int numberTwo;
	private MyCalculator calculator;
	
	@BeforeClass
	public static void setUpBeforeClass() throws Exception {
		numberOne = 12;
		numberTwo = 10;
	}
	
	@Before
	public void setUp() throws Exception {
		calculator = new MyCalculator();
	}

Kolejnymi krokami będzie wreszcie napisanie odpowiednich warunków testujących. Metoda testAdd zawiera trzy sekcje: given, when i then. W pierwszej sekcji zakładasz jakie warunki początkowe powinny istnieć w funkcjonalności, którą testujesz. Następnie w części when wykonujesz akcję, którą chcesz przetestować. W ostatniej linijce używasz asercji do porównania czy dwa warunki są ze sobą równe. Asercja to po prostu warunek, który musi być prawdziwy, aby kod wykonał się dalej. Jeśli on nie zajdzie, program kończy pracę właśnie w tym miejscu. W przypadku testów jednostkowych przerwanie pracy programu ogranicza się do jednego testu. Oznacza to, że test po prostu ‚nie przejdzie’.

Ostatnią rzeczą na którą możesz zwrócić uwagę jest metoda assertEquals, która jest metodą statyczną i została zaimportowana za pomocą statycznych importów.

@Test
public void testAdd() {
	int expected = 22; // given
	int actuals = calculator.add(numberOne, numberTwo); // when
	assertEquals(expected, actuals); //then
}

Kolejny test rozważa trochę inny przypadek. Czasami zachodzi potrzeba wyłączenia jakiegoś testu ze swojego systemu. Dzieje się tak na przykład, gdy test z niewiadomego powodu (np. po migracji bibliotek) nie może się wykonać, pomimo, że wszystkie inne testy oraz funkcjonalności kodu wygląda w porządku. Aby pominąć taką metodę, wystarczy dodać adnotację Ignore.

@Test
@Ignore
public void testMultiply() {
	int expected = 120;
	int actuals = calculator.multiply(numberOne, numberTwo);
	assertEquals(expected, actuals);
}

W następnym teście sprawdzam dzielenie dwóch liczb przez siebie. W efekcie wynikiem jest jakiś ułamek (typu zmiennoprzecinkowego). Ciekawostką tutaj jest to, że metoda assertEquals w przypadku typu double jest przeładowana (ang. overloaded) za pomocą podobnej metody z dodatkowym argumentem delta. Ten argument pozwala sprawdzić dwie liczby zmiennoprzecinkowe i niewymiernej części po przecinku. Dzieje się tak dlatego, że liczby zmiennoprzecinkowe często są zaokrąglane przez komputer i ich porównanie nie zawsze jest takie oczywiste*****.

@Test
public void testDivide() {		
	double expected = 1.2;
	double actuals = calculator.divide(numberOne, numberTwo);
	assertEquals(expected, actuals, 0);
}

Kolejny test zawiera parametr expected, którym możesz sprawdzić, czy przy wykonaniu danego testu wystąpi wyjątek. W tym przykładzie staram się dodać do liczby wartość null co skutkuje pojawieniem się NullPointerException.

@Test(expected = NullPointerException.class)
public void testAddWithNull() {
	int expected = 22;
	int actuals = calculator.add(numberOne, null);
	assertEquals(expected, actuals);
}

Ostatni test sprawdza dzielenie przez zero i tu ciekawostka, aby wykonać poprawną asercję należy użyć wartości infinity z klasy Double (w ten sposób Java radzi sobie z tym, że w matematyce nie można dzielić przez zero).

@Test
public void testDivideByZero() {
    double expected = Double.POSITIVE_INFINITY;
    double actuals = calculator.divide(numberOne, 0);
    assertEquals(expected, actuals, 0);
}

To wszystkie testy, które są potrzebne do przetestowania mojej klasy (oczywiście, jak chcesz możesz dodać swoje propozycje). Pamiętaj, że nigdy nie przetestujesz wszystkich możliwości, więc skup się na tych najbardziej prawdopodobnych, które mogą zajść.

Na koniec odpal swoje testy za pomocą wtyczki Eclipse’owej poprzez kliknięcie prawym przyciskiem myszy na klasie testowej i wybranie z menu opcji: Run as -> JUnit test.

Odpalenie testów

W efekcie powinien wyświetlić się taki wynik:

Wynik testów

Testy, które przeszły będą wyświetlone na zielono. Testy z błędami (asercja nie zwróciła prawdy), będą na czerwono. Jeśli test został źle zaimplementowany (np. nieprawidłowo zdefiniowano dane do testowania), to zostanie wyświetlony w sekcji Failures. Na szaro pokazano test, który został oznaczony adnotacją Ignore.

*Podobno napisał ją lecąc z kolegę samolotem. 😉

**Frameworkiem będziesz nazywać każdą zewnętrzną bibliotekę. Każdy z nich zawiera w sobie nic innego jak zwykły kod napisany w Javie.

***Jest to szczególnie przydatne w bardziej zaawansowanych projektach, gdzie specjalne narzędzia do budowania (np. maven) domyślnie wymagają trzymania testów w osobnych katalogach.

****Mocki to symulowane obiekty, które naśladują zachowanie ich rzeczywistych realizacji.

*****https://en.wikipedia.org/wiki/Floating-point_arithmetic

Dodaj komentarz