Przewodnik po solidnych testach jednostkowych i integracyjnych z JUnit
Opublikowany: 2022-03-11Zautomatyzowane testy oprogramowania mają krytyczne znaczenie dla długoterminowej jakości, łatwości konserwacji i rozszerzalności projektów oprogramowania, a dla Javy JUnit jest ścieżką do automatyzacji.
Podczas gdy większość tego artykułu skupi się na pisaniu solidnych testów jednostkowych i wykorzystaniu skrótów, mockowania i wstrzykiwania zależności, omówimy również testy JUnit i integracyjne.
Framework testowy JUnit jest powszechnym, bezpłatnym i otwartym narzędziem do testowania projektów opartych na Javie.
W chwili pisania tego tekstu, JUnit 4 jest obecnym głównym wydaniem, które zostało wydane ponad 10 lat temu, a ostatnia aktualizacja miała miejsce ponad dwa lata temu.
JUnit 5 (z modelami programowania i rozszerzenia Jupiter) jest w fazie rozwoju. Lepiej obsługuje funkcje językowe wprowadzone w Javie 8 i zawiera inne nowe, interesujące funkcje. Niektóre zespoły mogą uznać JUnit 5 za gotowy do użycia, podczas gdy inne mogą nadal używać JUnit 4 do momentu oficjalnego wydania 5. Przyjrzymy się przykładom z obu.
Bieg JUnit
Testy JUnit można uruchamiać bezpośrednio w IntelliJ, ale można je również uruchamiać w innych środowiskach IDE, takich jak Eclipse, NetBeans, a nawet w wierszu poleceń.
Testy powinny być zawsze uruchamiane w czasie kompilacji, zwłaszcza testy jednostkowe. Kompilację z dowolnymi testami, które zakończyły się niepowodzeniem, należy uznać za nieudaną, niezależnie od tego, czy problem tkwi w produkcji, czy w kodzie testowym – wymaga to od zespołu dyscypliny i gotowości do nadania najwyższego priorytetu rozwiązaniu problemów z testami, które zakończyły się niepowodzeniem, ale konieczne jest przestrzeganie duch automatyzacji.
Testy JUnit mogą być również uruchamiane i raportowane przez systemy ciągłej integracji, takie jak Jenkins. Projekty korzystające z narzędzi takich jak Gradle, Maven lub Ant mają tę dodatkową zaletę, że mogą uruchamiać testy w ramach procesu kompilacji.
Gradle
Jako przykładowy projekt Gradle dla JUnit 5, zobacz sekcję Gradle w podręczniku użytkownika JUnit i repozytorium junit5-samples.git. Zwróć uwagę, że może również uruchamiać testy korzystające z interfejsu API JUnit 4 (określanego jako „vintage” ).
Projekt można utworzyć w IntelliJ za pomocą opcji menu Plik > Otwórz… > przejdź do junit-gradle-consumer sub-directory
> OK > Otwórz jako projekt > OK, aby zaimportować projekt z Gradle.
W przypadku środowiska Eclipse wtyczkę Buildship Gradle można zainstalować z menu Pomoc > Eclipse Marketplace… Następnie projekt można zaimportować za pomocą opcji Plik > Importuj… > Gradle > Projekt Gradle > Dalej > Dalej > Przejdź do junit-gradle-consumer
> Dalej > Dalej > Zakończ.
Po skonfigurowaniu projektu Gradle w IntelliJ lub Eclipse uruchomienie zadania build
Gradle będzie obejmowało uruchomienie wszystkich testów JUnit z zadaniem test
. Należy zauważyć, że testy mogą zostać pominięte przy kolejnych wykonaniach build
, jeśli w kodzie nie zostaną wprowadzone żadne zmiany.
W przypadku JUnit 4 zobacz użycie JUnit z wiki Gradle.
Maven
W przypadku JUnit 5 zapoznaj się z sekcją Maven w podręczniku użytkownika i repozytorium junit5-samples.git, aby zapoznać się z przykładem projektu Maven. Może to również uruchomić stare testy (te, które używają interfejsu API JUnit 4).
W IntelliJ wybierz Plik > Otwórz… > przejdź do junit-maven-consumer/pom.xml
> OK > Otwórz jako projekt. Testy można następnie uruchomić z Maven Projects > junit5-maven-consumer > Lifecycle > Test.
W środowisku Eclipse wybierz polecenie Plik > Importuj… > Maven > Istniejące projekty Maven > Dalej > Przejdź do katalogu junit-maven-consumer
> Po wybraniu pom.xml
> Zakończ.
Testy można wykonać uruchamiając projekt jako Maven build… > określ cel test
> Uruchom.
W przypadku JUnit 4 zobacz JUnit w repozytorium Maven.
Środowiska programistyczne
Oprócz przeprowadzania testów za pomocą narzędzi do kompilacji, takich jak Gradle lub Maven, wiele IDE może bezpośrednio uruchamiać testy JUnit.
IntelliJ POMYSŁ
IntelliJ IDEA 2016.2 lub nowszy jest wymagany do testów JUnit 5, podczas gdy testy JUnit 4 powinny działać w starszych wersjach IntelliJ.
Na potrzeby tego artykułu możesz chcieć utworzyć nowy projekt w IntelliJ z jednego z moich repozytoriów GitHub ( JUnit5IntelliJ.git lub JUnit4IntelliJ.git), który zawiera wszystkie pliki w prostym przykładzie klasy Person
i korzysta z wbudowanego Biblioteki JUnit. Test można uruchomić za pomocą opcji Uruchom > Uruchom „Wszystkie testy”. Test można również uruchomić w IntelliJ z klasy PersonTest
.
Te repozytoria zostały utworzone za pomocą nowych projektów IntelliJ Java i budują struktury katalogów src/main/java/com/example
i src/test/java/com/example
. Katalog src/main/java
został określony jako folder źródłowy, podczas gdy src/test/java
został określony jako testowy folder źródłowy. Po utworzeniu klasy PersonTest
z metodą testową z adnotacją @Test
, kompilacja może się nie powieść, w takim przypadku IntelliJ proponuje dodanie JUnit 4 lub JUnit 5 do ścieżki klasy, którą można załadować z dystrybucji IntelliJ IDEA (zobacz te odpowiedzi na temat Stack Overflow, aby uzyskać więcej informacji). Na koniec dodano konfigurację uruchamiania JUnit dla wszystkich testów.
Zobacz także Wytyczne dotyczące testowania IntelliJ.
Zaćmienie
Pusty projekt Java w środowisku Eclipse nie będzie miał testowego katalogu głównego. Zostało to dodane z Właściwości projektu > Ścieżka budowania Java > Dodaj folder… > Utwórz nowy folder… > określ nazwę folderu > Zakończ. Nowy katalog zostanie wybrany jako folder źródłowy. Kliknij OK w obu pozostałych oknach dialogowych.
Testy JUnit 4 można utworzyć za pomocą opcji Plik > Nowy > Przypadek testowy JUnit. Wybierz „Nowy test JUnit 4” i nowo utworzony folder źródłowy dla testów. Określ „testowaną klasę” i „pakiet”, upewniając się, że pakiet pasuje do testowanej klasy. Następnie określ nazwę klasy testowej. Po zakończeniu kreatora, jeśli zostaniesz o to poproszony, wybierz „Dodaj bibliotekę JUnit 4” do ścieżki budowania. Projekt lub indywidualną klasę testową można następnie uruchomić jako test JUnit. Zobacz także Eclipse Writing and Running testów JUnit.
NetBeans
NetBeans obsługuje tylko testy JUnit 4. Klasy testowe można tworzyć w projekcie NetBeans Java za pomocą opcji Plik > Nowy plik… > Testy jednostkowe > Test JUnit lub Test dla istniejącej klasy. Domyślnie testowy katalog główny nosi nazwę test
w katalogu projektu.
Prosta klasa produkcyjna i jej przypadek testowy JUnit
Przyjrzyjmy się prostemu przykładowi kodu produkcyjnego i odpowiadającemu mu kodowi testu jednostkowego dla bardzo prostej klasy Person
. Możesz pobrać przykładowy kod z mojego projektu na github i otworzyć go przez IntelliJ.
src/main/java/com/example/Person.java
package com.example; class Person { private final String givenName; private final String surname; Person(String givenName, String surname) { this.givenName = givenName; this.surname = surname; } String getDisplayName() { return surname + ", " + givenName; } }
Niezmienna klasa Person
posiada konstruktor i getDisplayName()
. Chcemy sprawdzić, czy getDisplayName()
zwraca nazwę sformatowaną zgodnie z oczekiwaniami. Oto kod testu dla pojedynczego testu jednostkowego (JUnit 5):
src/test/java/com/przykład/PersonTest.java
package com.example; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class PersonTest { @Test void testGetDisplayName() { Person person = new Person("Josh", "Hayden"); String displayName = person.getDisplayName(); assertEquals("Hayden, Josh", displayName); } }
PersonTest
używa @Test
i asercji z JUnit 5. W przypadku JUnit 4 klasa i metoda PersonTest
muszą być publiczne i należy używać różnych importów. Oto przykładowy Gist JUnit 4.
Po uruchomieniu klasy PersonTest
w IntelliJ test przechodzi, a wskaźniki interfejsu użytkownika są zielone.
Wspólne konwencje JUnit
Nazywanie
Chociaż nie jest to wymagane, używamy wspólnych konwencji w nazewnictwie klasy testowej; konkretnie zaczynamy od nazwy testowanej klasy ( Person
) i dołączamy do niej „Test” ( PersonTest
). Nazywanie metody testowej jest podobne, zaczynając od testowanej metody ( getDisplayName()
) i poprzedzając ją „test” ( testGetDisplayName()
). Chociaż istnieje wiele innych całkowicie akceptowalnych konwencji dotyczących nazewnictwa metod testowych, ważne jest, aby zachować spójność w całym zespole i projekcie.
Nazwa w produkcji | Imię i nazwisko w testach |
---|---|
Osoba | Test osoby |
getDisplayName() | testDisplayName() |
Pakiety
Stosujemy również konwencję tworzenia klasy PersonTest
kodu testowego w tym samym pakiecie ( com.example
) co klasa Person
kodu produkcyjnego. Gdybyśmy użyli innego pakietu do testów, musielibyśmy użyć modyfikatora publicznego dostępu w klasach kodu produkcyjnego, konstruktorach i metodach, do których odwołują się testy jednostkowe, nawet jeśli nie jest to właściwe, więc lepiej jest trzymać je w tym samym pakiecie . Używamy jednak oddzielnych katalogów źródłowych ( src/main/java
i src/test/java
), ponieważ generalnie nie chcemy dołączać kodu testowego do wydanych kompilacji produkcyjnych.
Struktura i adnotacja
Adnotacja @Test
(JUnit 4/5) mówi JUnit, aby wykonał testGetDisplayName()
jako metodę testową i zgłosił, czy zakończyła się pomyślnie, czy nie. Dopóki wszystkie asercje (jeśli istnieją) przechodzą i nie są zgłaszane żadne wyjątki, test uznaje się za zaliczony.
Nasz kod testowy jest zgodny ze wzorcem struktury Arrange-Act-Assert (AAA). Inne typowe wzorce to Given-When-Then i Setup-Exercise-Verify-Teardown (Teardown zazwyczaj nie jest wyraźnie potrzebny do testów jednostkowych), ale w tym artykule używamy AAA.
Przyjrzyjmy się, jak nasz przykład testowy podąża za AAA. Pierwsza linia, „arrange” tworzy obiekt Person
, który będzie testowany:
Person person = new Person("Josh", "Hayden");
Drugi wiersz, „akt”, wykonuje metodę Person.getDisplayName()
kodu produkcyjnego:
String displayName = person.getDisplayName();
Trzecia linia, „asert”, weryfikuje, czy wynik jest zgodny z oczekiwaniami.
assertEquals("Hayden, Josh", displayName);
Wewnętrznie assertEquals()
używa metody równości obiektu String „Hayden, Josh” w celu sprawdzenia zgodności rzeczywistej wartości zwróconej z kodu produkcyjnego ( displayName
). Jeśli się nie zgadza, test zostanie oznaczony jako nieudany.
Zauważ, że testy często mają więcej niż jedną linię dla każdej z tych faz AAA.
Testy jednostkowe i kod produkcji
Teraz, gdy omówiliśmy już niektóre konwencje testowania, zwróćmy uwagę na umożliwienie testowania kodu produkcyjnego.
Wracamy do naszej klasy Person
, w której zaimplementowałem metodę zwracania wieku osoby na podstawie jej daty urodzenia. Przykłady kodu wymagają, aby Java 8 korzystała z nowych interfejsów API dat i funkcjonalnych. Oto jak wygląda nowa klasa Person.java
:
Osoba.java
// ... class Person { // ... private final LocalDate dateOfBirth; Person(String givenName, String surname, LocalDate dateOfBirth) { // ... this.dateOfBirth = dateOfBirth; } // ... long getAge() { return ChronoUnit.YEARS.between(dateOfBirth, LocalDate.now()); } public static void main(String... args) { Person person = new Person("Joey", "Doe", LocalDate.parse("2013-01-12")); System.out.println(person.getDisplayName() + ": " + person.getAge() + " years"); // Doe, Joey: 4 years } }
Prowadzenie tych zajęć (w momencie pisania) ogłasza, że Joey ma 4 lata. Dodajmy metodę testową:
OsobaTest.java
// ... class PersonTest { // ... @Test void testGetAge() { Person person = new Person("Joey", "Doe", LocalDate.parse("2013-01-12")); long age = person.getAge(); assertEquals(4, age); } }
Dziś mija, ale co z tym, kiedy uruchomimy go za rok? Ten test jest niedeterministyczny i kruchy, ponieważ oczekiwany wynik zależy od aktualnej daty uruchomienia testu przez system.
Karczowanie i wstrzykiwanie wartości dostawcy
Podczas uruchamiania w środowisku produkcyjnym chcemy użyć bieżącej daty, LocalDate.now()
, do obliczenia wieku osoby, ale aby przeprowadzić test deterministyczny nawet za rok, testy muszą dostarczyć własne wartości currentDate
.
Jest to znane jako wstrzykiwanie zależności. Nie chcemy, aby nasz obiekt Person
sam określał bieżącą datę, ale zamiast tego chcemy przekazać tę logikę jako zależność. Testy jednostkowe będą używać znanej, skróconej wartości, a kod produkcyjny umożliwi dostarczenie rzeczywistej wartości przez system w czasie wykonywania.
Dodajmy dostawcę LocalDate
do Person.java
:
Osoba.java
// ... class Person { // ... private final LocalDate dateOfBirth; private final Supplier<LocalDate> currentDateSupplier; Person(String givenName, String surname, LocalDate dateOfBirth) { this(givenName, surname, dateOfBirth, LocalDate::now); } // Visible for testing Person(String givenName, String surname, LocalDate dateOfBirth, Supplier<LocalDate> currentDateSupplier) { // ... this.dateOfBirth = dateOfBirth; this.currentDateSupplier = currentDateSupplier; } // ... long getAge() { return ChronoUnit.YEARS.between(dateOfBirth, currentDateSupplier.get()); } public static void main(String... args) { Person person = new Person("Joey", "Doe", LocalDate.parse("2013-01-12")); System.out.println(person.getDisplayName() + ": " + person.getAge() + " years"); // Doe, Joey: 4 years } }
Aby ułatwić testowanie metody getAge()
, zmieniliśmy ją tak, aby używała currentDateSupplier
, dostawcy LocalDate
, do pobierania bieżącej daty. Jeśli nie wiesz, kim jest dostawca, polecam poczytać o wbudowanych interfejsach funkcjonalnych Lambda.
Dodaliśmy również wstrzykiwanie zależności: nowy konstruktor testujący umożliwia testom dostarczanie własnych bieżących wartości dat. Oryginalny konstruktor wywołuje ten nowy konstruktor, przekazując statyczną referencję metody LocalDate::now
, która dostarcza obiekt LocalDate
, więc nasza główna metoda nadal działa jak poprzednio. A co z naszą metodą testową? Zaktualizujmy PersonTest.java
:
OsobaTest.java
// ... class PersonTest { // ... @Test void testGetAge() { LocalDate dateOfBirth = LocalDate.parse("2013-01-02"); LocalDate currentDate = LocalDate.parse("2017-01-17"); Person person = new Person("Joey", "Doe", dateOfBirth, ()->currentDate); long age = person.getAge(); assertEquals(4, age); } }
Test wstrzykuje teraz własną wartość currentDate
, więc nasz test nadal przejdzie, gdy zostanie uruchomiony w przyszłym roku lub w dowolnym roku. Jest to powszechnie nazywane skrótem lub podaniem znanej wartości do zwrócenia, ale najpierw musieliśmy zmienić Person
, aby umożliwić wstrzyknięcie tej zależności.
Zwróć uwagę na składnię lambda ( ()->currentDate
) podczas konstruowania obiektu Person
. Jest to traktowane jako dostawca LocalDate
, zgodnie z wymaganiami nowego konstruktora.
Naśmiewanie się z usługi sieciowej i zacinanie jej treści
Jesteśmy gotowi, aby nasz obiekt Person
— którego całe istnienie znajdowało się w pamięci JVM — komunikował się ze światem zewnętrznym. Chcemy dodać dwie metody: metodę publishAge()
, która opublikuje aktualny wiek osoby, oraz getThoseInCommon()
, która zwróci nazwiska sławnych osób, które mają te same urodziny lub są w tym samym wieku co nasz Person
. Załóżmy, że istnieje usługa RESTful, z którą możemy wchodzić w interakcje o nazwie „Urodziny osób”. Mamy do tego klienta Java, który składa się z jednej klasy BirthdaysClient
.
com.example.urodziny.UrodzinyKlient
package com.example.birthdays; import java.io.IOException; import java.util.Arrays; import java.util.Collection; public class BirthdaysClient { public void publishRegularPersonAge(String name, long age) throws IOException { System.out.println("publishing " + name + "'s age: " + age); // HTTP POST with name and age and possibly throw an exception } public Collection<String> findFamousNamesOfAge(long age) throws IOException { System.out.println("finding famous names of age " + age); return Arrays.asList(/* HTTP GET with age and possibly throw an exception */); } public Collection<String> findFamousNamesBornOn(int month, int dayOfMonth) throws IOException { System.out.println("finding famous names born on day " + dayOfMonth + " of month " + month); return Arrays.asList(/* HTTP GET with month and day and possibly throw an exception */); } }
Ulepszmy naszą klasę Person
. Zaczynamy od dodania nowej metody testowej dla pożądanego zachowania metody publishAge()
. Dlaczego warto zacząć od testu, a nie od funkcjonalności? Postępujemy zgodnie z zasadami programowania opartego na testach (znanych również jako TDD), w których najpierw piszemy test, a następnie kod, który go zaliczy.
OsobaTest.java
// … class PersonTest { // … @Test void testPublishAge() { LocalDate dateOfBirth = LocalDate.parse("2000-01-02"); LocalDate currentDate = LocalDate.parse("2017-01-01"); Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate); person.publishAge(); } }
W tym momencie kompilacja kodu testowego nie powiedzie się, ponieważ nie utworzyliśmy metody publishAge()
, którą on wywołuje. Po utworzeniu pustej metody Person.publishAge()
wszystko przechodzi. Jesteśmy teraz gotowi do testu, aby zweryfikować, czy wiek danej osoby został faktycznie opublikowany w BirthdaysClient
.

Dodawanie wykpiwanego obiektu
Ponieważ jest to test jednostkowy, powinien działać szybko i w pamięci, więc test utworzy nasz obiekt Person
z udawanym BirthdaysClient
, więc w rzeczywistości nie tworzy żądania internetowego. Następnie test użyje tego pozorowanego obiektu, aby sprawdzić, czy został wywołany zgodnie z oczekiwaniami. Aby to zrobić, dodamy zależność od frameworka Mockito (licencja MIT) do tworzenia makiety obiektów, a następnie utworzymy makietowany obiekt BirthdaysClient
:
OsobaTest.java
// ... import com.example.birthdays.BirthdaysClient; // ... import static org.mockito.Mockito.mock; class PersonTest { private BirthdaysClient birthdaysClient = mock(BirthdaysClient.class); // ... @Test void testPublishAge() { // ... Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate, birthdaysClient); // ... } }
Ponadto rozszerzyliśmy podpis konstruktora Person
, aby pobrać obiekt BirthdaysClient
, i zmieniliśmy test, aby wstrzyknąć zafałszowany obiekt BirthdaysClient
.
Dodawanie pozornego oczekiwania
Następnie dodajemy na końcu naszego testPublishAge
oczekiwanie na wywołanie BirthdaysClient
. Person.publishAge()
powinna go wywołać, jak pokazano w naszym nowym PersonTest.java
:
OsobaTest.java
// ... class PersonTest { // ... @Test void testPublishAge() throws IOException { // ... Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate, birthdaysClient); verifyZeroInteractions(birthdaysClient); person.publishAge(); verify(birthdaysClient).publishRegularPersonAge("Joe Sixteen", 16); } }
Nasz ulepszony w Mockito BirthdaysClient
śledzi wszystkie wywołania, które zostały wykonane z jego metodami, dzięki czemu weryfikujemy, czy żadne wywołania nie zostały wykonane do BirthdaysClient
za pomocą metody verifyZeroInteractions()
przed wywołaniem publishAge()
. Chociaż prawdopodobnie nie jest to konieczne, robiąc to, zapewniamy, że konstruktor nie wykonuje żadnych nieuczciwych wywołań. W wierszu verify()
określamy, jak ma wyglądać wywołanie BirthdaysClient
.
Zauważ, że ponieważ publishRegularPersonAge ma w swoim podpisie wyjątek IOException, dodajemy go również do podpisu naszej metody testowej.
W tym momencie test kończy się niepowodzeniem:
Wanted but not invoked: birthdaysClient.publishRegularPersonAge( "Joe Sixteen", 16L ); -> at com.example.PersonTest.testPublishAge(PersonTest.java:40)
Jest to oczekiwane, biorąc pod uwagę, że nie wdrożyliśmy jeszcze wymaganych zmian w Person.java
, ponieważ śledzimy rozwój oparty na testach. Teraz sprawimy, że ten test przejdzie, wprowadzając niezbędne zmiany:
Osoba.java
// ... class Person { // ... private final BirthdaysClient birthdaysClient; Person(String givenName, String surname, LocalDate dateOfBirth) { this(givenName, surname, dateOfBirth, LocalDate::now, new BirthdaysClient()); } // Visible for testing Person(String givenName, String surname, LocalDate dateOfBirth, Supplier<LocalDate> currentDateSupplier, BirthdaysClient birthdaysClient) { // ... this.birthdaysClient = birthdaysClient; } // ... void publishAge() { String nameToPublish = givenName + " " + surname; long age = getAge(); try { birthdaysClient.publishRegularPersonAge(nameToPublish, age); } catch (IOException e) { // TODO handle this! e.printStackTrace(); } } }
Testowanie pod kątem wyjątków
Konstruktor kodu produkcyjnego tworzy instancję nowego BirthdaysClient
, a publishAge()
wywołuje teraz birthdaysClient
. Wszystkie testy przechodzą; wszystko jest zielone. Świetnie! Ale zauważ, że publishAge()
połyka IOException. Zamiast pozwolić mu się wydostać, chcemy umieścić go w naszym własnym PersonException w nowym pliku o nazwie PersonException.java
:
PersonException.java
package com.example; public class PersonException extends Exception { public PersonException(String message, Throwable cause) { super(message, cause); } }
Wdrażamy ten scenariusz jako nową metodę testową w PersonTest.java
:
OsobaTest.java
// ... class PersonTest { // ... @Test void testPublishAge_IOException() throws IOException { LocalDate dateOfBirth = LocalDate.parse("2000-01-02"); LocalDate currentDate = LocalDate.parse("2017-01-01"); Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate, birthdaysClient); IOException ioException = new IOException(); doThrow(ioException).when(birthdaysClient).publishRegularPersonAge("Joe Sixteen", 16); try { person.publishAge(); fail("expected exception not thrown"); } catch (PersonException e) { assertSame(ioException, e.getCause()); assertEquals("Failed to publish Joe Sixteen age 16", e.getMessage()); } } }
Wywołanie Mockito doThrow()
birthdaysClient
, aby zgłosić wyjątek, gdy wywoływana jest metoda publishRegularPersonAge()
. Jeśli PersonException
nie zostanie zgłoszony, test zakończy się niepowodzeniem. W przeciwnym razie potwierdzamy, że wyjątek został prawidłowo powiązany z IOException i sprawdzamy, czy komunikat o wyjątku jest zgodny z oczekiwaniami. W tej chwili, ponieważ nie zaimplementowaliśmy żadnej obsługi w naszym kodzie produkcyjnym, nasz test kończy się niepowodzeniem, ponieważ oczekiwany wyjątek nie został zgłoszony. Oto, co musimy zmienić w Person.java
, aby test przeszedł:
Osoba.java
// ... class Person { // ... void publishAge() throws PersonException { // ... try { // ... } catch (IOException e) { throw new PersonException("Failed to publish " + nameToPublish + " age " + age, e); } } }
Stubs: Kiedy i Asercje
Teraz implementujemy Person.getThoseInCommon()
, dzięki czemu nasza klasa Person.Java
wygląda tak.
Nasz testGetThoseInCommon()
, w przeciwieństwie do testPublishAge()
, nie weryfikuje, czy zostały wykonane określone wywołania metod birthdaysClient
. Zamiast tego używa, when
wywołania skrótu zwracają wartości dla wywołań findFamousNamesOfAge()
i findFamousNamesBornOn()
, które getThoseInCommon()
będą musiały wykonać. Następnie zapewniamy, że wszystkie trzy podane przez nas nazwy zastępcze zostały zwrócone.
Opakowanie wielu asercji za pomocą metody assertAll()
JUnit 5 pozwala na sprawdzenie wszystkich asercji jako całości, zamiast zatrzymywania po pierwszej nieudanej asercji. Dołączamy również komunikat z assertTrue()
w celu zidentyfikowania konkretnych nazw, które nie zostały uwzględnione. Oto jak wygląda nasza metoda testowa „szczęśliwa ścieżka” (idealny scenariusz) (zauważ, że nie jest to solidny zestaw testów ze względu na „szczęśliwą ścieżkę”, ale o tym porozmawiamy później.
OsobaTest.java
// ... class PersonTest { // ... @Test void testGetThoseInCommon() throws IOException, PersonException { LocalDate dateOfBirth = LocalDate.parse("2000-01-02"); LocalDate currentDate = LocalDate.parse("2017-01-01"); Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate, birthdaysClient); when(birthdaysClient.findFamousNamesOfAge(16)).thenReturn(Arrays.asList("JoeFamous Sixteen", "Another Person")); when(birthdaysClient.findFamousNamesBornOn(1, 2)).thenReturn(Arrays.asList("Jan TwoKnown")); Set<String> thoseInCommon = person.getThoseInCommon(); assertAll( setContains(thoseInCommon, "Another Person"), setContains(thoseInCommon, "Jan TwoKnown"), setContains(thoseInCommon, "JoeFamous Sixteen"), ()-> assertEquals(3, thoseInCommon.size()) ); } private <T> Executable setContains(Set<T> set, T expected) { return () -> assertTrue(set.contains(expected), "Should contain " + expected); } // ... }
Utrzymuj kod testowy w czystości
Chociaż często jest to pomijane, równie ważne jest, aby kod testowy był wolny od ropiejących duplikatów. Czysty kod i zasady takie jak „nie powtarzaj się” są bardzo ważne dla utrzymania wysokiej jakości kodu bazowego, produkcyjnego i testowego. Zauważ, że najnowszy PersonTest.java ma pewne zduplikowane teraz, gdy mamy kilka metod testowania.
Aby to naprawić, możemy zrobić kilka rzeczy:
Wyodrębnij obiekt IOException do prywatnego pola końcowego.
Wyodrębnij utworzenie obiektu
Person
do jego własnej metody (w tym przypadkucreateJoeSixteenJan2()
, ponieważ większość obiektów Person jest tworzonych z tymi samymi parametrami.Utwórz
assertCauseAndMessage()
dla różnych testów, które weryfikująPersonExceptions
.
Wyniki czystego kodu można zobaczyć w tej wersji pliku PersonTest.java.
Testuj więcej niż szczęśliwą ścieżkę
Co powinniśmy zrobić, gdy data urodzenia obiektu Person
jest późniejsza niż data bieżąca? Defekty w aplikacjach są często spowodowane nieoczekiwanymi danymi wejściowymi lub brakiem przewidywania w przypadkach narożników, krawędzi lub granic. Ważne jest, aby starać się przewidywać te sytuacje najlepiej, jak potrafimy, a testy jednostkowe są często odpowiednim miejscem do tego. Tworząc nasze Person
i PersonTest
, uwzględniliśmy kilka testów dla oczekiwanych wyjątków, ale w żadnym wypadku nie były one kompletne. Na przykład używamy LocalDate
, który nie reprezentuje ani nie przechowuje danych o strefie czasowej. Nasze wywołania LocalDate.now()
zwracają jednak wartość LocalDate
opartą na domyślnej strefie czasowej systemu, która może być o dzień wcześniejsza lub późniejsza niż strefa czasowa użytkownika systemu. Czynniki te należy wziąć pod uwagę przy wdrażaniu odpowiednich testów i zachowań.
Należy również przetestować granice. Rozważmy obiekt Person
z metodą getDaysUntilBirthday()
. Testowanie powinno obejmować to, czy urodziny danej osoby minęły już w bieżącym roku, czy urodziny tej osoby są dzisiaj i jak rok przestępny wpływa na liczbę dni. Scenariusze te można uwzględnić, sprawdzając jeden dzień przed urodzinami danej osoby, dzień i jeden dzień po urodzinach danej osoby, gdzie następny rok jest rokiem przestępnym. Oto odpowiedni kod testowy:
OsobaTest.java
// ... class PersonTest { private final Supplier<LocalDate> currentDateSupplier = ()-> LocalDate.parse("2015-05-02"); private final LocalDate ageJustOver5 = LocalDate.parse("2010-05-01"); private final LocalDate ageExactly5 = LocalDate.parse("2010-05-02"); private final LocalDate ageAlmost5 = LocalDate.parse("2010-05-03"); // ... @Test void testGetDaysUntilBirthday() { assertAll( createPersonAndAssertValue(ageAlmost5, 1, Person::getDaysUntilBirthday), createPersonAndAssertValue(ageExactly5, 0, Person::getDaysUntilBirthday), createPersonAndAssertValue(ageJustOver5, 365, Person::getDaysUntilBirthday) ); } private Executable createPersonAndAssertValue(LocalDate dateOfBirth, long expectedValue, Function<Person, Long> personLongFunction) { Person person = new Person("Given", "Sur", dateOfBirth, currentDateSupplier); long actualValue = personLongFunction.apply(person); return () -> assertEquals(expectedValue, actualValue); } }
Testy integracyjne
Skupiliśmy się głównie na testach jednostkowych, ale JUnit może być również używany do testów integracyjnych, akceptacyjnych, funkcjonalnych i systemowych. Takie testy często wymagają więcej kodu konfiguracyjnego, np. uruchamianie serwerów, ładowanie baz danych ze znanymi danymi itp. Chociaż często możemy uruchomić tysiące testów jednostkowych w ciągu kilku sekund, uruchomienie dużych zestawów testów integracyjnych może zająć minuty, a nawet godziny. Testy integracyjne generalnie nie powinny być używane do próby pokrycia każdej permutacji lub ścieżki w kodzie; bardziej odpowiednie są do tego testy jednostkowe.
Tworzenie testów dla aplikacji internetowych, które napędzają przeglądarki internetowe w wypełnianiu formularzy, klikaniu przycisków, oczekiwaniu na załadowanie treści itp., jest powszechnie wykonywane przy użyciu Selenium WebDriver (licencja Apache 2.0) w połączeniu z „Wzorcem obiektu strony” (zobacz wiki github SeleniumHQ oraz artykuł Martina Fowlera na temat Page Objects).
JUnit jest skuteczny w testowaniu RESTful API przy użyciu klienta HTTP, takiego jak Apache HTTP Client lub Spring Rest Template (dobrym przykładem jest HowToDoInJava.com).
W naszym przypadku z obiektem Person
test integracji może polegać na użyciu rzeczywistego BirthdaysClient
, a nie próbnego, z konfiguracją określającą bazowy adres URL usługi People Birthdays. Test integracyjny wykorzystywał następnie testową instancję takiej usługi, sprawdzał, czy daty urodzin zostały w niej opublikowane i tworzył w usłudze znane osoby, które zostaną zwrócone.
Inne funkcje JUnit
JUnit ma wiele dodatkowych funkcji, których jeszcze nie zbadaliśmy w przykładach. Opiszemy niektóre i dostarczymy referencje dla innych.
Urządzenia testowe
Należy zauważyć, że JUnit tworzy nową instancję klasy testowej do uruchamiania każdej metody @Test
. JUnit zapewnia również zaczepy adnotacji do uruchamiania określonych metod przed lub po wszystkich lub każdej z metod @Test
. Te zaczepy są często używane do konfigurowania lub czyszczenia bazy danych lub obiektów pozorowanych i różnią się między JUnit 4 i 5.
JUnit 4 | JUnit 5 | Dla metody statycznej? |
---|---|---|
@BeforeClass | @BeforeAll | TAk |
@AfterClass | @AfterAll | TAk |
@Before | @BeforeEach | Nie |
@After | @AfterEach | Nie |
W naszym przykładzie PersonTest
wybraliśmy konfigurację obiektu makiety BirthdaysClient
w samych metodach @Test
, ale czasami trzeba zbudować bardziej złożone struktury makiety obejmujące wiele obiektów. @BeforeEach
(w JUnit 5) i @Before
(w JUnit 4) są do tego często odpowiednie.
Adnotacje @After*
są bardziej powszechne w testach integracyjnych niż w testach jednostkowych, ponieważ wyrzucanie elementów bezużytecznych JVM obsługuje większość obiektów utworzonych na potrzeby testów jednostkowych. @BeforeClass
i @BeforeAll
są najczęściej używane w testach integracyjnych, które wymagają jednorazowego wykonania kosztownych czynności konfiguracyjnych i rozłączania, a nie dla każdej metody testowej.
W przypadku JUnit 4 należy zapoznać się z przewodnikiem po uchwytach testowych (ogólne koncepcje nadal obowiązują w przypadku JUnit 5).
Zestawy testowe
Czasami chcesz uruchomić wiele powiązanych testów, ale nie wszystkie. W takim przypadku grupy testów można składać w zestawy testów. Aby dowiedzieć się, jak to zrobić w JUnit 5, zapoznaj się z artykułem HowToProgram.xyz o JUnit 5 oraz w dokumentacji zespołu JUnit dla JUnit 4.
@Nested i @DisplayName w JUnit 5
JUnit 5 dodaje możliwość używania niestatycznych zagnieżdżonych klas wewnętrznych, aby lepiej pokazać relacje między testami. Powinno to być dobrze znane tym, którzy pracowali z zagnieżdżonymi opisami w frameworkach testowych, takich jak Jasmine dla JavaScript. Klasy wewnętrzne są oznaczone adnotacją @Nested
, aby tego użyć.
Adnotacja @DisplayName
jest również nowością w JUnit 5, umożliwiając opisanie testu do raportowania w formacie ciągu, który ma być wyświetlany oprócz identyfikatora metody testowej.
Chociaż @Nested
i @DisplayName
mogą być używane niezależnie od siebie, razem mogą zapewnić bardziej przejrzyste wyniki testów opisujące zachowanie systemu.
Zapałki Hamcrest
Framework Hamcrest, choć sam nie jest częścią kodu JUnit, stanowi alternatywę dla tradycyjnych metod asercji w testach, pozwalając na bardziej wyrazisty i czytelny kod testowy. Zobacz poniższą weryfikację przy użyciu zarówno tradycyjnego attachEquals, jak i potwierdzenia HamcrestThat:
//Traditional assert assertEquals("Hayden, Josh", displayName); //Hamcrest assert assertThat(displayName, equalTo("Hayden, Josh"));
Hamcrest może być używany zarówno z JUnit 4, jak i 5. Samouczek Vogella.com na temat Hamcrest jest dość obszerny.
Dodatkowe zasoby
Artykuł Testy jednostkowe, Jak napisać testowalny kod i Dlaczego to ma znaczenie, zawiera bardziej szczegółowe przykłady pisania czystego, testowalnego kodu.
Build with Confidence: A Guide to JUnit Tests analizuje różne podejścia do testowania jednostkowego i integracyjnego oraz dlaczego najlepiej wybrać jedno i trzymać się go
JUnit 4 Wiki i JUnit 5 User Guide są zawsze doskonałym punktem odniesienia.
Dokumentacja Mockito zawiera informacje o dodatkowych funkcjonalnościach i przykładach.
JUnit to droga do automatyzacji
Zbadaliśmy wiele aspektów testowania w świecie Java za pomocą JUnit. Przyjrzeliśmy się testom jednostkowym i integracyjnym przy użyciu frameworku JUnit dla baz kodu Java, integracji JUnit w środowiskach programistycznych i kompilacji, sposobom korzystania z mocków i stubów z dostawcami i Mockito, powszechnym konwencjom i najlepszym praktykom kodu, co testować i niektórym inne wspaniałe funkcje JUnit.
Teraz kolej na czytelnika, aby umiejętnie stosować, utrzymywać i czerpać korzyści z automatycznych testów przy użyciu frameworku JUnit.