Dziś nie znajdziesz aplikacji, która nie byłaby usłana testami, więc ten temat będzie bardziej odpowiedni niż kiedykolwiek dla początkujących programistów: nie możesz odnieść sukcesu bez testów. Zastanówmy się, jakie rodzaje testów są zasadniczo stosowane, a następnie szczegółowo przestudiujemy wszystko, co trzeba wiedzieć o testach jednostkowych.
Rodzaje testów
Co to jest test? Według Wikipedii: „Testowanie oprogramowania obejmuje wykonanie komponentu oprogramowania lub komponentu systemu w celu oceny jednej lub więcej interesujących właściwości”. Innymi słowy jest to sprawdzenie poprawności działania naszego systemu w określonych sytuacjach. Zobaczmy, jakie są ogólnie rodzaje testów:- Testy jednostkowe — Testy, których celem jest sprawdzenie każdego modułu systemu z osobna. Testy te powinny dotyczyć najmniejszych atomowych części systemu, np. modułów.
- Testowanie systemu — Testowanie wysokiego poziomu w celu sprawdzenia działania większej części aplikacji lub systemu jako całości.
- Testy regresyjne — Testowanie, które służy do sprawdzenia, czy nowe funkcje lub poprawki błędów wpływają na istniejącą funkcjonalność aplikacji lub wprowadzają stare błędy.
- Testy funkcjonalne — Sprawdzenie, czy część aplikacji spełnia wymagania określone w specyfikacjach, historiach użytkowników itp.
Rodzaje testów funkcjonalnych:
- Testy białoskrzynkowe — Sprawdzenie, czy część aplikacji spełnia wymagania, znając wewnętrzną implementację systemu;
- Testy czarnoskrzynkowe — Sprawdzanie, czy część aplikacji spełnia wymagania bez znajomości wewnętrznej implementacji systemu.
- Testy wydajności — testy napisane w celu określenia, jak system lub część systemu działa pod określonym obciążeniem.
- Testowanie obciążenia — Testy mające na celu sprawdzenie stabilności systemu przy standardowych obciążeniach oraz znalezienie maksymalnego obciążenia, przy którym aplikacja nadal działa poprawnie.
- Testy warunków skrajnych — Testy mające na celu sprawdzenie wydajności aplikacji pod niestandardowymi obciążeniami oraz określenie maksymalnego obciążenia przed awarią systemu.
- Testy bezpieczeństwa — Testy używane do sprawdzania bezpieczeństwa systemu (przed hakerami, wirusami, nieautoryzowanym dostępem do poufnych danych i innymi zachwycającymi atakami).
- Testy lokalizacji — Testy lokalizacji aplikacji.
- Testy użyteczności — Testy mające na celu sprawdzenie użyteczności, zrozumiałości, atrakcyjności i łatwości uczenia się.
- Jednostka — ta sekcja dotyczy testów jednostkowych, które są stosowane w różnych warstwach aplikacji. Testują najmniejszą podzielną jednostkę logiki aplikacji. Na przykład klasy, ale najczęściej metody. Testy te zwykle starają się w jak największym stopniu odizolować to, co jest testowane, od jakiejkolwiek zewnętrznej logiki. Oznacza to, że próbują stworzyć iluzję, że reszta aplikacji działa zgodnie z oczekiwaniami.
Zawsze powinno być dużo tych testów (więcej niż jakikolwiek inny typ), ponieważ testują małe fragmenty i są bardzo lekkie, nie zużywają dużo zasobów (czyli pamięci RAM i czasu).
- Integracja — ta sekcja dotyczy testowania integracji. Ten test sprawdza większe części systemu. Oznacza to, że albo łączy kilka elementów logiki (kilka metod lub klas), albo sprawdza poprawność interakcji z komponentem zewnętrznym. Testy te są zwykle mniejsze niż testy jednostkowe, ponieważ są cięższe.
Przykładem testu integracyjnego może być połączenie z bazą danych i sprawdzenie poprawności działania metod pracy z nią.
- Interfejs użytkownika — ta sekcja dotyczy testów sprawdzających działanie interfejsu użytkownika. Obejmują one logikę na wszystkich poziomach aplikacji, dlatego nazywane są również testami end-to-end. Z reguły jest ich znacznie mniej, ponieważ są najbardziej uciążliwe i muszą sprawdzać najbardziej potrzebne (używane) ścieżki.
Na powyższym obrazku widzimy, że różne części trójkąta różnią się rozmiarem: mniej więcej takie same proporcje istnieją w liczbie różnych rodzajów testów w rzeczywistej pracy.
Dzisiaj przyjrzymy się bliżej najpopularniejszym testom, jednostkowym, ponieważ każdy szanujący się programista Java powinien umieć z nich korzystać na podstawowym poziomie.
Kluczowe pojęcia w testach jednostkowych
Pokrycie testowe (pokrycie kodu) jest jedną z głównych miar tego, jak dobrze aplikacja jest testowana. Jest to procent kodu, który jest objęty testami (0-100%). W praktyce wielu dąży do tego odsetka jako celu. To jest coś, z czym się nie zgadzam, ponieważ oznacza to, że testy zaczynają być stosowane tam, gdzie nie są potrzebne. Załóżmy na przykład, że mamy standardowe operacje CRUD (tworzenie/pobieranie/aktualizowanie/usuwanie) w naszej usłudze bez dodatkowej logiki. Metody te są czysto pośrednikami, delegującymi pracę do warstwy pracującej z repozytorium. W tej sytuacji nie mamy czego testować, może poza tym, czy dana metoda wywołuje metodę DAO, ale to żart. Do oceny pokrycia testu zwykle używane są dodatkowe narzędzia: JaCoCo, Cobertura, Clover, Emma itp. Aby uzyskać bardziej szczegółowe studium tego tematu,- Pokrycie kodu na średnim
- Testowanie czarnej skrzynki: szczegółowy samouczek z przykładami i technikami
- Piszemy nasz test.
- Przeprowadzamy test. Nic dziwnego, że kończy się niepowodzeniem, ponieważ nie zaimplementowaliśmy jeszcze wymaganej logiki.
- Dodaj kod, który powoduje przejście testu (uruchamiamy test ponownie).
- Refaktoryzujemy kod.
Etapy testowania
Test składa się z trzech etapów:- Określ dane testowe (uchwyty).
- Wykonaj testowany kod (wywołaj testowaną metodę).
- Zweryfikuj wyniki i porównaj z oczekiwanymi wynikami.
Środowiska testowe
A więc do rzeczy. Istnieje kilka środowisk testowych (frameworków) dostępnych dla Javy. Najpopularniejsze z nich to JUnit i TestNG. Do naszej recenzji tutaj używamy: Test JUnit to metoda w klasie, która jest używana tylko do testowania. Klasa jest zwykle nazywana tak samo jak klasa, którą testuje, z dodanym na końcu słowem „Test”. Na przykład CarService -> CarServiceTest. System kompilacji Maven automatycznie włącza takie klasy do zakresu testowego. W rzeczywistości ta klasa jest nazywana klasą testową. Omówmy pokrótce podstawowe adnotacje:- @Test wskazuje, że metoda jest testem (w zasadzie metoda oznaczona tą adnotacją jest testem jednostkowym).
- @Before oznacza metodę, która zostanie wykonana przed każdym testem. Na przykład, aby wypełnić klasę danymi testowymi, odczytać dane wejściowe itp.
- @After służy do oznaczenia metody, która będzie wywoływana po każdym teście (np. w celu wyczyszczenia danych lub przywrócenia wartości domyślnych).
- @BeforeClass jest umieszczony nad metodą, analogicznie do @Before. Ale taka metoda jest wywoływana tylko raz przed wszystkimi testami dla danej klasy i dlatego musi być statyczna. Służy do wykonywania operacji wymagających większej ilości zasobów, takich jak uruchamianie testowej bazy danych.
- @AfterClass jest przeciwieństwem @BeforeClass: jest wykonywany raz dla danej klasy, ale dopiero po wszystkich testach. Służy na przykład do czyszczenia trwałych zasobów lub odłączania się od bazy danych.
- @Ignore oznacza, że metoda jest wyłączona i zostanie zignorowana podczas całego testu. Jest to używane w różnych sytuacjach, na przykład, jeśli podstawowa metoda została zmieniona, a test nie został jeszcze przerobiony, aby uwzględnić zmiany. W takich przypadkach pożądane jest również dodanie opisu, np. @Ignore("Jakiś opis").
- @Test(expected = Exception.class) jest używany do testów negatywnych. Są to testy, które sprawdzają, jak metoda zachowuje się w przypadku błędu, czyli oczekuje, że metoda zgłosi jakiś wyjątek. Na taką metodę wskazuje adnotacja @Test, ale ze wskazaniem, który błąd wychwycić.
- @Test(timeout = 100) sprawdza, czy metoda jest wykonywana w czasie nie dłuższym niż 100 milisekund.
- @Mock jest używany nad polem do przypisania fałszywego obiektu (to nie jest adnotacja JUnit, ale zamiast tego pochodzi z Mockito). W razie potrzeby ustawiamy zachowanie makiety dla konkretnej sytuacji bezpośrednio w metodzie testowej.
- @RunWith(MockitoJUnitRunner.class) jest umieszczony nad klasą. Ta adnotacja mówi JUnit, aby wywołał testy w klasie. Istnieją różne biegacze, w tym: MockitoJUnitRunner, JUnitPlatform i SpringRunner. W JUnit 5 adnotacja @RunWith została zastąpiona potężniejszą adnotacją @ExtendWith.
- assertEquals(Oczekiwany obiekt, Rzeczywisty obiekt) — sprawdza, czy przekazane obiekty są sobie równe.
- assertTrue(boolean flag) — sprawdza, czy przekazana wartość jest prawdziwa.
- assertFalse(boolean flag) — sprawdza, czy przekazana wartość jest fałszywa.
- assertNull(Object object) — sprawdza, czy przekazany obiekt ma wartość null.
- assertSame(Object firstObject, Object secondObject) — sprawdza, czy przekazane wartości odnoszą się do tego samego obiektu.
- assertThat(T t, Matcher
dopasowujący) — Sprawdza, czy t spełnia warunek określony w matcher.
Testowanie w praktyce
Przyjrzyjmy się teraz powyższemu materiałowi na konkretnym przykładzie. Przetestujemy metodę aktualizacji usługi. Nie będziemy rozważać warstwy DAO, ponieważ używamy domyślnej. Dodajmy starter do testów:<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>2.2.2.RELEASE</version>
<scope>test</scope>
</dependency>
A tutaj mamy klasę usług:
@Service
@RequiredArgsConstructor
public class RobotServiceImpl implements RobotService {
private final RobotDAO robotDAO;
@Override
public Robot update(Long id, Robot robot) {
Robot found = robotDAO.findById(id);
return robotDAO.update(Robot.builder()
.id(id)
.name(robot.getName() != null ? robot.getName() : found.getName())
.cpu(robot.getCpu() != null ? robot.getCpu() : found.getCpu())
.producer(robot.getProducer() != null ? robot.getProducer() : found.getProducer())
.build());
}
}
Linia 8 — pobierz zaktualizowany obiekt z bazy danych. Linie 9-14 — utwórz obiekt za pomocą konstruktora. Jeśli obiekt przychodzący ma pole, ustaw je. Jeśli nie, zostawimy to, co jest w bazie danych. A teraz spójrz na nasz test:
@RunWith(MockitoJUnitRunner.class)
public class RobotServiceImplTest {
@Mock
private RobotDAO robotDAO;
private RobotServiceImpl robotService;
private static Robot testRobot;
@BeforeClass
public static void prepareTestData() {
testRobot = Robot
.builder()
.id(123L)
.name("testRobotMolly")
.cpu("Intel Core i7-9700K")
.producer("China")
.build();
}
@Before
public void init() {
robotService = new RobotServiceImpl(robotDAO);
}
Linia 1 — nasz Biegacz. Linia 4 — izolujemy usługę od warstwy DAO, podstawiając mock. Linia 11 — ustawiamy obiekt testowy (ten, którego użyjemy jako królik doświadczalny) dla klasy. Linia 22 — ustawiamy obiekt usługi, czyli to, co będziemy testować.
@Test
public void updateTest() {
when(robotDAO.findById(any(Long.class))).thenReturn(testRobot);
when(robotDAO.update(any(Robot.class))).then(returnsFirstArg());
Robot robotForUpdate = Robot
.builder()
.name("Vally")
.cpu("AMD Ryzen 7 2700X")
.build();
Robot resultRobot = robotService.update(123L, robotForUpdate);
assertNotNull(resultRobot);
assertSame(resultRobot.getId(),testRobot.getId());
assertThat(resultRobot.getName()).isEqualTo(robotForUpdate.getName());
assertTrue(resultRobot.getCpu().equals(robotForUpdate.getCpu()));
assertEquals(resultRobot.getProducer(),testRobot.getProducer());
}
Widzimy tutaj, że test ma trzy wyraźne podziały: Wiersze 3-9 — określające urządzenia. Linia 11 — wykonanie testowanego kodu. Wiersze 13-17 — sprawdzanie wyników. Bardziej szczegółowo: Linie 3-4 — ustaw zachowanie dla makiety DAO. Linia 5 — ustaw instancję, którą będziemy aktualizować zgodnie z naszym standardem. Linia 11 — użyj metody i weź wynikową instancję. Linia 13 — sprawdź, czy nie jest pusta. Linia 14 — porównaj identyfikator wyniku i podane argumenty metody. Linia 15 — sprawdź, czy nazwa została zaktualizowana. Linia 16 — zobacz wynik procesora. Linia 17 — nie określiliśmy tego pola w instancji, więc powinno pozostać bez zmian. Tutaj sprawdzamy ten warunek. Uruchommy to:Test jest zielony! Możemy odetchnąć z ulgą :) Podsumowując, testowanie poprawia jakość kodu oraz sprawia, że proces tworzenia jest bardziej elastyczny i niezawodny. Wyobraź sobie, ile wysiłku wymaga przeprojektowanie oprogramowania obejmującego setki plików klas. Kiedy mamy napisane testy jednostkowe dla wszystkich tych klas, możemy z pewnością refaktoryzować. A co najważniejsze, pomaga nam łatwo znaleźć błędy podczas tworzenia. Chłopaki i dziewczęta, to wszystko, co mam dzisiaj. Daj lajka i zostaw komentarz :)