CodeGym /Blog Java /Random-PL /Wszystko o testowaniu jednostkowym: techniki, koncepcje, ...
John Squirrels
Poziom 41
San Francisco

Wszystko o testowaniu jednostkowym: techniki, koncepcje, praktyka

Opublikowano w grupie Random-PL
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. Wszystko o testowaniu jednostkowym: techniki, koncepcje, praktyka - 1

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ę.
Wszystko to brzmi dobrze, ale jak to działa w praktyce? Prosty! Korzystamy z piramidy testowej Mike'a Cohna: Wszystko o testowaniu jednostkowym: techniki, koncepcje, praktyka - 2Jest to uproszczona wersja piramidy: teraz jest podzielona na jeszcze mniejsze części. Ale dzisiaj nie będziemy zbyt wyrafinowani. Rozważymy najprostszą wersję.
  1. 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).

  2. 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ą.

  3. 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, TDD oznacza rozwój oparty na testach. W tym podejściu, zanim zrobisz cokolwiek innego, piszesz test, który sprawdzi konkretny kod. Okazuje się, że jest to testowanie czarnej skrzynki: wiemy, jakie są dane wejściowe i wiemy, jakie powinny być dane wyjściowe. Pozwala to uniknąć powielania kodu. Programowanie sterowane testami rozpoczyna się od zaprojektowania i opracowania testów dla każdego fragmentu funkcjonalności aplikacji. W podejściu TDD najpierw tworzymy test, który definiuje i testuje zachowanie kodu. Głównym celem TDD jest uczynienie kodu bardziej zrozumiałym, prostszym i wolnym od błędów. Wszystko o testowaniu jednostkowym: techniki, koncepcje, praktyka - 3Podejście składa się z następujących elementów:
  • 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.
TDD opiera się na testach jednostkowych, ponieważ są to najmniejsze elementy składowe piramidy automatyzacji testów. Dzięki testom jednostkowym możemy przetestować logikę biznesową dowolnej klasy. BDD oznacza rozwój oparty na zachowaniu. Podejście to opiera się na TDD. Mówiąc dokładniej, wykorzystuje przykłady prostego języka, które wyjaśniają zachowanie systemu dla wszystkich osób zaangażowanych w programowanie. Nie będziemy zagłębiać się w ten termin, ponieważ dotyczy on głównie testerów i analityków biznesowych. Przypadek testowy to scenariusz opisujący kroki, określone warunki i parametry wymagane do sprawdzenia testowanego kodu. Osprzęt testowy to kod, który konfiguruje środowisko testowe tak, aby miało stan niezbędny do pomyślnego uruchomienia testowanej metody. Jest to predefiniowany zestaw obiektów i ich zachowania w określonych warunkach.

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.
Wszystko o testowaniu jednostkowym: techniki, koncepcje, praktyka - 4Aby zapewnić modułowość testów, należy odizolować je od innych warstw aplikacji. Można to zrobić za pomocą stubów, mocków i szpiegów. Mocki to obiekty, które można dostosować (na przykład dostosować do każdego testu). Pozwalają nam określić, czego oczekujemy od wywołań metod, czyli oczekiwanych odpowiedzi. Używamy symulowanych obiektów, aby sprawdzić, czy otrzymujemy to, czego oczekujemy. Kody pośredniczące zapewniają zakodowaną na stałe odpowiedź na wywołania podczas testowania. Mogą również przechowywać informacje o połączeniu (np. parametry połączenia lub liczbę połączeń). Czasami nazywa się ich szpiegami. Czasami ludzie mylą pojęcia „stub” i „mock”: różnica polega na tym, że kod pośredniczący niczego nie sprawdza — jedynie symuluje dany stan. Mock to obiekt, który ma oczekiwania. Na przykład, że dana metoda musi zostać wywołana określoną liczbę razy. Innymi słowy,

Ś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: Wszystko o testach jednostkowych: techniki, koncepcje, praktyka - 5Test 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.
Rzućmy okiem na niektóre metody stosowane do porównywania wyników:

  • 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.
AssertJ zapewnia również użyteczną metodę porównania: assertThat(firstObject).isEqualTo(secondObject) . Tutaj wspomniałem o podstawowych metodach — pozostałe są odmianami powyższych.

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:Wszystko o testowaniu jednostkowym: techniki, koncepcje, praktyka - 6Test 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 :)
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION