Samouczek JUnit 5, część 1: Testy jednostkowe z JUnit 5, Mockito i Hamcrest

JUnit 5 jest nowym de facto standardem do tworzenia testów jednostkowych w Javie. Ta najnowsza wersja pozostawiła za sobą ograniczenia języka Java 5 i zintegrowała wiele funkcji z języka Java 8, w szczególności obsługę wyrażeń lambda.

W pierwszej połowie dwuczęściowego wprowadzenia do JUnit 5 zaczniesz od testowania z JUnit 5. Pokażę ci, jak skonfigurować projekt Maven do korzystania z JUnit 5, jak pisać testy przy użyciu @Testi @ParameterizedTestadnotacji, i jak pracować z nowymi adnotacjami cyklu życia w JUnit 5. Zobaczysz także krótki przykład użycia tagów filtrów, a ja pokażę ci, jak zintegrować JUnit 5 z biblioteką asercji innej firmy - w tym przypadku Hamcrest . Na koniec otrzymasz szybkie wprowadzenie samouczka do integracji JUnit 5 z Mockito, dzięki czemu będziesz mógł pisać bardziej niezawodne testy jednostkowe dla złożonych, rzeczywistych systemów.

pobierz Pobierz kod Pobierz kod źródłowy przykładów w tym samouczku. Stworzone przez Stevena Hainesa dla JavaWorld.

Rozwój oparty na testach

Jeśli tworzyłeś kod w Javie od jakiegoś czasu, prawdopodobnie dobrze znasz programowanie sterowane testami, więc opiszę tę sekcję krótko. Ważne jest jednak, aby zrozumieć, dlaczego piszemy testy jednostkowe, a także strategie, które programiści stosują podczas projektowania testów jednostkowych.

Rozwój oparty na testach (TDD) to proces tworzenia oprogramowania, który łączy w sobie kodowanie, testowanie i projektowanie. Jest to podejście polegające na testowaniu, które ma na celu poprawę jakości Twoich aplikacji. Rozwój oparty na testach jest definiowany przez następujący cykl życia:

  1. Dodaj test.
  2. Uruchom wszystkie testy i zobacz, jak nowy test się nie powiódł.
  3. Zaimplementuj kod.
  4. Uruchom wszystkie testy i zobacz, czy nowy test się powiódł.
  5. Refaktoryzuj kod.

Rysunek 1 przedstawia ten cykl życia TDD.

Steven Haines

Pisanie testów przed napisaniem kodu ma dwojaki cel. Po pierwsze, zmusza Cię do przemyślenia problemu biznesowego, który próbujesz rozwiązać. Na przykład, jak powinny zachowywać się udane scenariusze? Jakie warunki powinny zawieść? Jak mieliby zawieść? Po drugie, najpierw testowanie daje większe zaufanie do testów. Ilekroć piszę testy po napisaniu kodu, zawsze muszę je łamać, aby upewnić się, że faktycznie wychwytują błędy. Pisanie testów najpierw pozwala uniknąć tego dodatkowego kroku.

Pisanie testów dla szczęśliwej ścieżki jest zwykle łatwe: biorąc pod uwagę dobre dane wejściowe, klasa powinna zwrócić deterministyczną odpowiedź. Jednak pisanie negatywnych (lub zakończonych niepowodzeniem) przypadków testowych, szczególnie dla złożonych komponentów, może być bardziej skomplikowane.

Jako przykład rozważ napisanie testów dla repozytorium bazy danych. Na szczęśliwej ścieżce wstawiamy rekord do bazy danych i otrzymujemy z powrotem utworzony obiekt wraz z wygenerowanymi kluczami. W rzeczywistości musimy również wziąć pod uwagę możliwość wystąpienia konfliktu, na przykład wstawienia rekordu z unikalną wartością kolumny, która jest już przechowywana przez inny rekord. Dodatkowo, co się dzieje, gdy repozytorium nie może połączyć się z bazą danych, być może z powodu zmiany nazwy użytkownika lub hasła? Co się stanie, jeśli podczas przesyłania wystąpi błąd sieci? Co się stanie, jeśli żądanie nie zostanie zakończone w określonym limicie czasu?

Aby zbudować niezawodny komponent, należy wziąć pod uwagę wszystkie prawdopodobne i mało prawdopodobne scenariusze, opracować dla nich testy i napisać kod, który spełni te testy. W dalszej części artykułu przyjrzymy się strategiom tworzenia różnych scenariuszy awarii, a także niektórym nowym funkcjom w JUnit 5, które mogą pomóc w testowaniu tych scenariuszy.

Przyjęcie JUnit 5

Jeśli używasz JUnit od jakiegoś czasu, niektóre zmiany w JUnit 5 będą stanowić korektę. Oto ogólne podsumowanie różnic między tymi dwiema wersjami:

  • JUnit 5 jest teraz spakowany w org.junit.jupitergrupie, co zmienia sposób, w jaki umieścisz go w projektach Maven i Gradle.
  • JUnit 4 wymaga co najmniej JDK o wartości 5 JDK; JUnit 5 wymaga co najmniej JDK 8.
  • JUnit 4 na @Before, @BeforeClass, @After, i @AfterClassadnotacje zostały zastąpione przez @BeforeEach, @BeforeAll, @AfterEach, i @AfterAll, odpowiednio.
  • @IgnoreAdnotacja JUnit 4 została zastąpiona @Disabledadnotacją.
  • @CategoryAdnotacja została zastąpiona przez @Tagadnotacji.
  • JUnit 5 dodaje nowy zestaw metod asercji.
  • Runners zostały zastąpione rozszerzeniami, z nowym API dla implementatorów rozszerzeń.
  • JUnit 5 wprowadza założenia, które uniemożliwiają wykonanie testu.
  • JUnit 5 obsługuje zagnieżdżone i dynamiczne klasy testowe.

W tym artykule omówimy większość tych nowych funkcji.

Testowanie jednostkowe za pomocą JUnit 5

Zacznijmy od prostego, kompleksowego przykładu konfigurowania projektu do używania JUnit 5 do testów jednostkowych. Listing 1 przedstawia MathToolsklasę, której metoda konwertuje licznik i mianownik na double.

Listing 1. Przykładowy projekt JUnit 5 (MathTools.java)

 package com.javaworld.geekcap.math; public class MathTools { public static double convertToDecimal(int numerator, int denominator) { if (denominator == 0) { throw new IllegalArgumentException("Denominator must not be 0"); } return (double)numerator / (double)denominator; } }

Mamy dwa podstawowe scenariusze testowania MathToolsklasy i jej metody:

  • Ważne badanie , w którym przechodzimy niezerowe liczby całkowite dla licznika i mianownika.
  • Scenariusz awarii , w którym możemy przekazać wartość zerową dla mianownika.

Listing 2 przedstawia klasę testową JUnit 5 do testowania tych dwóch scenariuszy.

Listing 2. Klasa testowa JUnit 5 (MathToolsTest.java)

 package com.javaworld.geekcap.math; import java.lang.IllegalArgumentException; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; class MathToolsTest { @Test void testConvertToDecimalSuccess() { double result = MathTools.convertToDecimal(3, 4); Assertions.assertEquals(0.75, result); } @Test void testConvertToDecimalInvalidDenominator() { Assertions.assertThrows(IllegalArgumentException.class, () -> MathTools.convertToDecimal(3, 0)); } }

Na liście 2 testConvertToDecimalInvalidDenominatormetoda wykonuje MathTools::convertToDecimalmetodę wewnątrz assertThrowswywołania. Pierwszy argument to oczekiwany typ zgłaszanego wyjątku. Drugi argument to funkcja, która zgłosi ten wyjątek. assertThrowsSposób realizuje funkcję i potwierdza, że oczekiwany typ wyjątku.

Klasa Assertions i jej metody

org.junit.jupiter.api.TestAdnotacja oznacza metodę testową. Zauważ, że @Testadnotacja pochodzi teraz z pakietu JUnit 5 Jupiter API zamiast z pakietu JUnit 4 org.junit. testConvertToDecimalSuccessSposób pierwszy przeprowadza się MathTools::convertToDecimalsposobem w liczniku i mianowniku 3 4, a następnie stwierdza, że wynik jest równy 0,75. org.junit.jupiter.api.AssertionsKlasa zapewnia zestaw staticmetod do porównywania rzeczywistych i oczekiwanych rezultatów. AssertionsKlasa ma następujące metody, które obejmują większość prymitywnych typów danych:

  • assertArrayEquals porównuje zawartość rzeczywistej tablicy z oczekiwaną tablicą.
  • assertEquals porównuje rzeczywistą wartość z wartością oczekiwaną.
  • assertNotEquals porównuje dwie wartości, aby sprawdzić, czy nie są równe.
  • assertTrue sprawdza, czy podana wartość jest prawdziwa.
  • assertFalse sprawdza, czy podana wartość jest fałszywa.
  • assertLinesMatchporównuje dwie listy Strings.
  • assertNull validates that the provided value is null.
  • assertNotNull validates that the provided value is not null.
  • assertSame validates that two values reference the same object.
  • assertNotSame validates that two values do not reference the same object.
  • assertThrows validates that the execution of a method throws an expected exception (you can see this in the testConvertToDecimalInvalidDenominator example above).
  • assertTimeout validates that a supplied function completes within a specified timeout.
  • assertTimeoutPreemptively validates that a supplied function completes within a specified timeout, but once the timeout is reached it kills the function's execution.

If any of these assertion methods fail, the unit test is marked as failed. That failure notice will be written to the screen when you run the test, then saved in a report file.

Using delta with assertEquals

When using float and double values in an assertEquals, you can also specify a delta that represents a threshold of difference between the two. In our example we could have added a delta of 0.001, in case 0.75 was actually returned as 0.750001.

Analyzing your test results

In addition to validating a value or behavior, the assert methods can also accept a textual description of the error, which can help you diagnose failures. For example:

 Assertions.assertEquals(0.75, result, "The MathTools::convertToDecimal value did not return the correct value of 0.75 for 3/4"); Assertions.assertEquals(0.75, result, () -> "The MathTools::convertToDecimal value did not return the correct value of 0.75 for 3/4"); 

The output will show the expected value of 0.75 and the actual value. It will also display the specified message, which can help you understand the context of the error. The difference between the two variations is that the first one always creates the message, even if it is not displayed, whereas the second one only constructs the message if the assertion fails. In this case, the construction of the message is trivial, so it doesn't really matter. Still, there is no need to construct an error message for a test that passes, so it's usually a best practice to use the second style.

Finally, if you're using an IDE like IntelliJ to run your tests, each test method will be displayed by its method name. This is fine if your method names are readable, but you can also add a @DisplayName annotation to your test methods to better identify the tests:

@Test @DisplayName("Test successful decimal conversion") void testConvertToDecimalSuccess() { double result = MathTools.convertToDecimal(3, 4); Assertions.assertEquals(0.751, result); }

Running your unit test

In order to run JUnit 5 tests from a Maven project, you need to include the maven-surefire-plugin in the Maven pom.xml file and add a new dependency. Listing 3 shows the pom.xml file for this project.

Listing 3. Maven pom.xml for an example JUnit 5 project

  4.0.0 com.javaworld.geekcap junit5 jar 1.0-SNAPSHOT    org.apache.maven.plugins maven-compiler-plugin 3.8.1  8 8    org.apache.maven.plugins maven-surefire-plugin 3.0.0-M4    junit5 //maven.apache.org   org.junit.jupiter junit-jupiter 5.6.0 test   

JUnit 5 dependencies

JUnit 5 packages its components in the org.junit.jupiter group and we need to add the junit-jupiter artifact, which is an aggregator artifact that imports the following dependencies:

  • junit-jupiter-api defines the API for writing tests and extensions.
  • junit-jupiter-engine to implementacja silnika testowego, która uruchamia testy jednostkowe.
  • junit-jupiter-params zapewnia obsługę testów parametryzowanych.

Następnie musimy dodać maven-surefire-pluginwtyczkę kompilacji, aby uruchomić testy.

Na koniec pamiętaj, aby dołączyć maven-compiler-pluginwersję Java 8 lub nowszą, aby móc korzystać z funkcji Java 8, takich jak lambdy.

Uruchom!

Użyj następującego polecenia, aby uruchomić klasę testową ze swojego IDE lub z Maven:

mvn clean test

Jeśli ci się powiedzie, powinieneś zobaczyć wyniki podobne do następujących:

 [INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] ------------------------------------------------------- [INFO] Running com.javaworld.geekcap.math.MathToolsTest [INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.04 s - in com.javaworld.geekcap.math.MathToolsTest [INFO] [INFO] Results: [INFO] [INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 3.832 s [INFO] Finished at: 2020-02-16T08:21:15-05:00 [INFO] ------------------------------------------------------------------------