JavaRush /Blog Java /Random-PL /Testowanie jednostkowe Java: techniki, koncepcje, praktyk...

Testowanie jednostkowe Java: techniki, koncepcje, praktyka

Opublikowano w grupie Random-PL
Dziś prawie nie znajdziesz aplikacji, która nie byłaby pokryta testami, więc ten temat będzie bardziej istotny niż kiedykolwiek dla początkujących programistów: bez testów nie można nigdzie się dostać. W ramach reklamy sugeruję przejrzenie moich poprzednich artykułów. Niektóre z nich obejmują testy (a mimo to artykuły będą bardzo przydatne):
  1. Testowanie integracji bazy danych przy użyciu MariaDB w celu zastąpienia MySql
  2. Wdrożenie aplikacji wielojęzycznej
  3. Zapisywanie plików do aplikacji oraz danych o nich do bazy danych
Zastanówmy się, jakie rodzaje testów są w zasadzie stosowane, a następnie szczegółowo przestudiujemy wszystko, co musisz wiedzieć o testach jednostkowych.

Rodzaje testów

Co to jest test? Jak mówi Wiki: „ Test lub test to sposób badania podstawowych procesów systemu poprzez umieszczanie systemu w różnych sytuacjach i śledzenie obserwowalnych w nim zmian”. Inaczej mówiąc, jest to test poprawności działania naszego systemu w określonych sytuacjach. Wszystko o testowaniu jednostkowym: metody, koncepcje, praktyka - 2Zobaczmy, jakie są rodzaje testów:
  1. Testy jednostkowe to testy, których zadaniem jest przetestowanie każdego modułu systemu z osobna. Pożądane jest, aby były to minimalnie podzielne elementy systemu, na przykład moduły.

  2. Testowanie systemu to test wysokiego poziomu, mający na celu przetestowanie działania większego fragmentu aplikacji lub systemu jako całości.

  3. Testowanie regresyjne to testowanie, które służy do sprawdzenia, czy nowe funkcje lub poprawki błędów wpływają na istniejącą funkcjonalność aplikacji i czy pojawiają się ponownie stare błędy.

  4. Testowanie funkcjonalne polega na sprawdzeniu zgodności części aplikacji z wymaganiami określonymi w specyfikacjach, historiach użytkownika itp.

    Rodzaje testów funkcjonalnych:

    • Test „białej skrzynki” na zgodność części aplikacji z wymaganiami ze znajomością wewnętrznego wdrożenia systemu;
    • Test „czarnej skrzynki” na zgodność części aplikacji z wymaganiami bez wiedzy o wewnętrznej implementacji systemu.
  5. Testowanie wydajności to rodzaj testów pisanych w celu określenia prędkości, z jaką system lub jego część działa pod określonym obciążeniem.
  6. Testy obciążeniowe - testy mające na celu sprawdzenie stabilności systemu pod standardowymi obciążeniami i znalezienie maksymalnego możliwego szczytu, przy którym aplikacja działa poprawnie.
  7. Testy obciążeniowe to rodzaj testów mających na celu sprawdzenie funkcjonalności aplikacji pod niestandardowymi obciążeniami i określenie maksymalnego możliwego szczytu, przy którym system nie ulegnie awarii.
  8. Testy bezpieczeństwa - testy służące do sprawdzenia bezpieczeństwa systemu (przed atakami hakerów, wirusami, nieautoryzowanym dostępem do poufnych danych i innymi przyjemnościami życia).
  9. Testowanie lokalizacji to testowanie lokalizacji aplikacji.
  10. Testowanie użyteczności to rodzaj testów mających na celu sprawdzenie użyteczności, zrozumiałości, atrakcyjności i łatwości uczenia się dla użytkowników.
  11. Wszystko brzmi nieźle, ale jak to wygląda w praktyce? To proste: zastosowano piramidę testową Mike'a Cohna: Wszystko o testowaniu jednostkowym: metody, koncepcje, praktyka - 4Jest to uproszczona wersja piramidy: teraz jest ona podzielona na mniejsze części. Ale dzisiaj nie będziemy wypaczać i rozważać najprostszej opcji.
    1. Jednostka - testy jednostkowe stosowane w różnych warstwach aplikacji, testujące najmniejszą podzielną logikę aplikacji: na przykład klasę, ale najczęściej metodę. Testy te zazwyczaj starają się jak najbardziej odizolować od logiki zewnętrznej, czyli stworzyć iluzję, że reszta aplikacji pracuje w trybie standardowym.

      Takich testów powinno być zawsze dużo (więcej niż inne typy), ponieważ testują małe fragmenty i są bardzo lekkie, a przy tym nie zużywają dużo zasobów (przez zasoby mam na myśli pamięć RAM i czas).

    2. Integracja - testowanie integracyjne. Sprawdza większe fragmenty systemu, czyli jest albo kombinacją kilku fragmentów logiki (kilku metod lub klas), albo poprawnością pracy z komponentem zewnętrznym. Zwykle jest mniej takich testów niż testów jednostkowych, ponieważ są one cięższe.

      Jako przykład testów integracyjnych można rozważyć połączenie się z bazą danych i sprawdzenie poprawności wykonania metod z nią pracujących .

    3. UI - testy sprawdzające działanie interfejsu użytkownika. Wpływają na logikę na wszystkich poziomach aplikacji, dlatego nazywane są również end-to-end. Z reguły jest ich znacznie mniej, ponieważ są najciężsi i muszą sprawdzać najbardziej potrzebne (używane) ścieżki.

      Na powyższym rysunku widzimy stosunek pól różnych części trójkąta: liczba tych testów w rzeczywistej pracy zachowuje w przybliżeniu tę samą proporcję.

      Dzisiaj przyjrzymy się bliżej najczęściej używanym testom - testom jednostkowym, gdyż każdy szanujący się programista Java powinien umieć z nich korzystać na podstawowym poziomie.

    Kluczowe pojęcia testów jednostkowych

    Pokrycie testów (Code Coverage) to jedna z głównych ocen jakości testowania aplikacji. Jest to procent kodu objęty testami (0-100%). W praktyce wiele osób goni za tym procentem, z czym się nie zgadzam, bo zaczynają dodawać testy tam, gdzie nie są potrzebne. Przykładowo nasza usługa posiada standardowe operacje CRUD (utwórz/pobierz/aktualizuj/usuń) bez dodatkowej logiki. Metody te są jedynie pośrednikami delegującymi pracę do warstwy współpracującej z repozytorium. W tej sytuacji nie mamy nic do testowania: być może, czy ta metoda wywołuje metodę z Tao, ale to nie jest poważne. Aby ocenić zasięg testów, zwykle stosuje się dodatkowe narzędzia: JaCoCo, Cobertura, Clover, Emma itp. Aby uzyskać bardziej szczegółowe badanie tego problemu, zachowaj kilka odpowiednich artykułów: TDD (Rozwój oparty na testach) - rozwój oparty na testach. W tym podejściu najpierw pisze się test, który sprawdzi konkretny kod. Okazuje się, że jest to test czarnej skrzynki: wiemy, co jest na wejściu i wiemy, co powinno się wydarzyć na wyjściu. Pozwala to uniknąć powielania kodu. Rozwój oparty na testach rozpoczyna się od zaprojektowania i opracowania testów dla każdej małej funkcjonalności aplikacji. W podejściu TDD najpierw opracowywany jest test, który definiuje i weryfikuje, co zrobi kod. Głównym celem TDD jest uczynienie kodu jaśniejszym, prostszym i wolnym od błędów. Wszystko o testowaniu jednostkowym: metody, koncepcje, praktyka - 6Podejście składa się z następujących elementów:
    1. Piszemy nasz test.
    2. Przeprowadzamy test, czy zdał, czy nie (widzimy, że wszystko jest czerwone - nie panikuj: tak powinno być).
    3. Dodajemy kod, który powinien spełnić ten test (uruchom test).
    4. Refaktoryzujemy kod.
    Opierając się na tym, że testy jednostkowe są najmniejszymi elementami piramidy automatyzacji testów, na nich opiera się TDD. Za pomocą testów jednostkowych możemy przetestować logikę biznesową dowolnej klasy. BDD (rozwój zorientowany na zachowanie) – rozwój poprzez zachowanie. Podejście to opiera się na TDD. Mówiąc dokładniej, wykorzystuje przykłady napisane jasnym językiem (zwykle po angielsku), które ilustrują zachowanie systemu dla wszystkich zaangażowanych w rozwój. Nie będziemy się zagłębiać w to pojęcie, gdyż dotyczy ono głównie testerów i analityków biznesowych. Przypadek Testowy – skrypt opisujący kroki, konkretne warunki i parametry niezbędne do weryfikacji implementacji testowanego kodu. Fixture to stan środowiska testowego niezbędny do pomyślnego wykonania testowanej metody. Jest to z góry określony zbiór obiektów i ich zachowania w zastosowanych warunkach.

    Etapy testowania

    Test składa się z trzech etapów:
    1. Określanie danych do testowania (urządzenia).
    2. Użycie testowanego kodu (wywołanie testowanej metody).
    3. Sprawdzanie wyników i porównywanie ich z oczekiwanymi.
    Wszystko o testowaniu jednostkowym: metody, koncepcje, praktyka - 7Aby zapewnić modułowość testów, należy odizolować się od innych warstw aplikacji. Można to zrobić za pomocą fragmentatorów, prób i szpiegów. Makiety to obiekty, które można dostosowywać (np. specyficzne dla każdego testu) i które pozwalają ustawić oczekiwania dotyczące wywołań metod w postaci odpowiedzi, które planujemy otrzymać. Sprawdzanie oczekiwań odbywa się poprzez wywołania obiektów Mock. Stubs — zapewniają przewodową odpowiedź na wywołania podczas testowania. Mogą również przechowywać informacje o połączeniu (na przykład parametry lub liczbę tych połączeń). Czasami nazywa się je ich własnym terminem – szpieg ( Spy ). Czasami te terminy stub i mock są mylone: ​​różnica polega na tym, że stub niczego nie sprawdza, a jedynie symuluje dany stan. Makieta to przedmiot, który ma oczekiwania. Na przykład, że dana metoda klasy musi zostać wywołana określoną liczbę razy. Innymi słowy, Twój test nigdy nie zostanie przerwany z powodu kodu pośredniczącego, ale może zostać przerwany z powodu próby.

    Środowiska testowe

    Przejdźmy więc teraz do rzeczy. Istnieje kilka środowisk testowych (frameworków) dostępnych dla języka Java. Najpopularniejsze z nich to JUnit i TestNG. Do naszego przeglądu używamy: Wszystko o testowaniu jednostkowym: metody, koncepcje, praktyka - 8Test JUnit to metoda zawarta w klasie, która jest używana tylko do testowania. Klasa ma zazwyczaj taką samą nazwę jak klasa, którą testuje, z opcją +Test na końcu. Na przykład CarService → CarServiceTest. System kompilacji Mavena automatycznie uwzględnia takie klasy w obszarze testowym. W rzeczywistości ta klasa nazywa się klasą testową. Przejrzyjmy trochę podstawowe adnotacje: @Test - definicja tej metody jako metody testowej (w rzeczywistości metoda oznaczona tą adnotacją jest testem jednostkowym). @Before - zaznacza metodę, która będzie wykonywana przed każdym testem. Np. wypełnienie danych testowych klasy, odczyt danych wejściowych itp. @After - umieszczony nad metodą, która będzie wywoływana po każdym teście (czyszczenie danych, przywracanie wartości domyślnych). @BeforeClass - umieszczony nad metodą - analogicznie do @Before. Jednak ta metoda jest wywoływana tylko raz przed wszystkimi testami dla danej klasy i dlatego musi być statyczna. Służy do wykonywania bardziej wymagających operacji, takich jak podnoszenie testowej bazy danych. @AfterClass jest przeciwieństwem @BeforeClass: wykonywane raz dla danej klasy, ale wykonywane po wszystkich testach. Używany na przykład do czyszczenia trwałych zasobów lub odłączania się od bazy danych. @Ignore - zauważa, że ​​poniższa metoda jest wyłączona i będzie ignorowana podczas ogólnego uruchamiania testów. Stosuje się go w różnych przypadkach, na przykład, jeśli zmieniono metodę podstawową i nie było czasu na ponowne wykonanie testu. W takich przypadkach wskazane jest również dodanie opisu - @Ignore("Jakiś opis"). @Test (oczekiwany = wyjątek.klasa) - używany w przypadku 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. Taka metoda jest oznaczona adnotacją @Test, ale zawiera błąd do wyłapania. @Test(timeout=100) - sprawdza, czy metoda zostanie wykonana w czasie nie dłuższym niż 100 milisekund. @Mock - nad polem używana jest klasa, aby ustawić dany obiekt jako mock (nie jest to z biblioteki Junit, tylko z Mockito), a jeśli będzie taka potrzeba, ustawimy zachowanie makiety w konkretnej sytuacji bezpośrednio w metodzie badawczej. @RunWith(MockitoJUnitRunner.class) - metoda umieszczana jest nad klasą. Jest to przycisk umożliwiający uruchomienie w nim testów. Runnery mogą być różne: na przykład są to: MockitoJUnitRunner, JUnitPlatform, SpringRunner itp.). W JUnit 5 adnotacja @RunWith została zastąpiona mocniejszą adnotacją @ExtendWith. Przyjrzyjmy się niektórym metodom porównywania wyników:
    • assertEquals(Object expecteds, Object actuals)— sprawdza, czy przesyłane obiekty są równe.
    • assertTrue(boolean flag)— sprawdza, czy przekazana wartość zwraca wartość true.
    • assertFalse(boolean flag)— sprawdza, czy przekazana wartość zwraca wartość false.
    • assertNull(Object object)– sprawdza, czy obiekt ma wartość null.
    • assertSame(Object firstObject, Object secondObject)— sprawdza, czy przekazane wartości odnoszą się do tego samego obiektu.
    • assertThat(T t, Matcher<T> matcher)— sprawdza, czy t spełnia warunek określony w dopasowaniu.
    Istnieje również przydatny formularz porównawczy z Asserj - assertThat(firstObject).isEqualTo(secondObject) Tutaj mówiłem o podstawowych metodach, ponieważ reszta to różne odmiany powyższych.

    Praktyka testowania

    Przyjrzyjmy się teraz powyższemu materiałowi na konkretnym przykładzie. Przetestujemy metodę dla usługi - aktualizacja. Nie będziemy rozważać warstwy dao, ponieważ jest ona naszą domyślną. 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>
    Zatem klasa 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());
       }
    }
    8 - wyciągnij zaktualizowany obiekt z bazy danych 9-14 - utwórz obiekt za pomocą konstruktora, jeśli przychodzący obiekt ma pole - ustaw je, jeśli nie - zostaw to, co jest w bazie danych I 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);
       }
    1 — nasz Runner 4 — odizoluj usługę od warstwy dao, podstawiając próbkę 11 — ustaw jednostkę testową dla klasy (tę, której będziemy używać jako chomika testowego) 22 — ustaw obiekt usługi, który 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 wyraźny podział testu na trzy części: 3-9 – ustawienie urządzeń 11 – wykonanie testowanej części 13-17 – sprawdzenie wyników Więcej szczegółów: 3-4 – ustawienie zachowania moka dao 5 – ustawienie instancja, którą zaktualizujemy na bazie naszego standardu 11 - użyj metody i weź wynikową instancję 13 - sprawdź, czy nie jest równa zero 14 - sprawdź identyfikator wyniku i podane argumenty metody 15 - sprawdź, czy nazwa została zaktualizowana 16 - spójrz na wynik dla procesora 17 - ponieważ nie ustawiliśmy tego w polu instancji aktualizacji, powinno pozostać takie samo, sprawdźmy to. Wszystko o testowaniu jednostkowym: metody, koncepcje, praktyka - 9Zacznijmy: Wszystko o testowaniu jednostkowym: techniki, koncepcje, praktyka - 10Test jest zielony, można odetchnąć)) Podsumujmy: testowanie poprawia jakość kodu i sprawia, że ​​proces programowania jest bardziej elastyczny i niezawodny. Wyobraź sobie, ile wysiłku musielibyśmy włożyć w przeprojektowanie oprogramowania zawierającego setki plików klas. Kiedy już mamy napisane testy jednostkowe dla wszystkich tych klas, możemy z pewnością przeprowadzić refaktoryzację. A co najważniejsze, pomaga nam łatwo znaleźć błędy podczas programowania. Chłopaki, to wszystko dla mnie dzisiaj: lajkujcie, piszcie komentarze))) Wszystko o testowaniu jednostkowym: metody, koncepcje, praktyka - 11
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION