Урок за JUnit 5, част 1: Тестване на модули с JUnit 5, Mockito и Hamcrest

JUnit 5 е новият де факто стандарт за разработване на модулни тестове в Java. Тази най-нова версия остави зад себе си ограниченията на Java 5 и интегрира много функции от Java 8, най-вече поддръжката на ламбда изрази.

В тази първа половина от двучастично въведение в JUnit 5 ще започнете с тестване с JUnit 5. Ще ви покажа как да конфигурирате проект на Maven да използва JUnit 5, как да пишете тестове с помощта на @Testи @ParameterizedTestанотациите, и как да работите с новите анотации на жизнения цикъл в JUnit 5. Ще видите също кратък пример за използване на филтърни тагове и ще ви покажа как да интегрирате JUnit 5 с библиотека за твърдения на трети страни - в този случай Hamcrest . И накрая, ще получите кратко ръководство за интегриране на JUnit 5 с Mockito, така че да можете да пишете по-стабилни модулни тестове за сложни системи от реалния свят.

изтегляне Вземете кода Вземете изходния код за примери в този урок. Създадено от Стивън Хейнс за JavaWorld.

Тествано развитие

Ако сте разработвали Java код за някакъв период от време, вероятно сте добре запознати с разработеното чрез тестове, така че ще запазя този раздел накратко. Важно е да разберем защо пишем модулни тестове, както и стратегиите, които разработчиците използват при проектирането на модулни тестове.

Тестовото развитие (TDD) е процес на разработване на софтуер, който преплита кодиране, тестване и дизайн. Това е първоначален тест, който има за цел да подобри качеството на вашите приложения. Тестовото развитие се определя от следния жизнен цикъл:

  1. Добавете тест.
  2. Изпълнете всичките си тестове и наблюдавайте неуспеха на новия тест.
  3. Внедрете кода.
  4. Изпълнете всичките си тестове и наблюдавайте успеха на новия тест.
  5. Рефакторирайте кода.

Фигура 1 показва този жизнен цикъл на TDD.

Стивън Хейнс

Има две цели да пишете тестове, преди да напишете кода си. Първо, това ви принуждава да мислите за бизнес проблема, който се опитвате да разрешите. Например как трябва да се държат успешните сценарии? Какви условия трябва да се провалят? Как трябва да се провалят? Второ, първо тестването ви дава повече увереност във вашите тестове. Винаги, когато пиша тестове след писане на код, винаги трябва да ги разбивам, за да съм сигурен, че всъщност улавят грешки. Писането на тестове първо избягва тази допълнителна стъпка.

Писането на тестове за щастливия път обикновено е лесно: При добър вход класът трябва да върне детерминиран отговор. Но писането на отрицателни (или неуспешни) тестови случаи, особено за сложни компоненти, може да бъде по-сложно.

Като пример, помислете за писане на тестове за хранилище на база данни. По щастливия път вмъкваме запис в базата данни и получаваме обратно създадения обект, включително всички генерирани ключове. В действителност трябва да разгледаме и възможността за конфликт, като например вмъкване на запис с уникална стойност на колоната, която вече се съхранява от друг запис. Освен това, какво се случва, когато хранилището не може да се свърже с базата данни, може би защото потребителското име или паролата са се променили? Какво се случва, ако има грешка в мрежата при транспортиране? Какво се случва, ако заявката не завърши в определеното от вас време за изчакване?

За да създадете надежден компонент, трябва да разгледате всички вероятни и малко вероятни сценарии, да разработите тестове за тях и да напишете кода си, за да ги удовлетворите. По-нататък в статията ще разгледаме стратегии за създаване на различни сценарии на отказ, заедно с някои от новите функции в JUnit 5, които могат да ви помогнат да тествате тези сценарии.

Приемане на JUnit 5

Ако използвате JUnit от известно време, някои от промените в JUnit 5 ще бъдат корекция. Ето обобщение на високо ниво за разликите между двете версии:

  • JUnit 5 вече е опакован в org.junit.jupiterгрупата, което променя начина, по който ще го включите във вашите проекти Maven и Gradle.
  • JUnit 4 изисква минимум JDK от JDK 5; JUnit 5 изисква минимум JDK 8.
  • JUnit 4 е @Before, @BeforeClass, @After, и @AfterClassпояснения бяха заменени от @BeforeEach, @BeforeAll, @AfterEach, и @AfterAll, съответно.
  • @IgnoreАнотацията на JUnit 4 е заменена с @Disabledанотацията.
  • В @Categoryанотацията е заменен от @Tagанотацията.
  • JUnit 5 добавя нов набор от методи за твърдение.
  • Бегачите са заменени с разширения, с нов API за реализатори на разширения.
  • JUnit 5 въвежда предположения, които спират изпълнението на тест.
  • JUnit 5 поддържа вложени и динамични тестови класове.

Ще разгледаме повечето от тези нови функции в тази статия.

Единично тестване с JUnit 5

Нека започнем просто, с пример от край до край за конфигуриране на проект за използване на JUnit 5 за единичен тест. Листинг 1 показва MathToolsклас, чийто метод преобразува числител и знаменател в a double.

Листинг 1. Примерен проект на 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; } }

Имаме два основни сценария за тестване на MathToolsкласа и неговия метод:

  • А валиден тест , при който се минава, различни от нула числа за числител и знаменател.
  • А сценарий недостатъчност , в който се минава с нулева стойност за знаменател.

Листинг 2 показва JUnit 5 тестов клас за тестване на тези два сценария.

Листинг 2. Тестов клас 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)); } }

In Listing 2, the testConvertToDecimalInvalidDenominator method executes the MathTools::convertToDecimal method inside an assertThrows call. The first argument is the expected type of exception to be thrown. The second argument is a function that will throw that exception. The assertThrows method executes the function and validates that the expected type of exception is thrown.

The Assertions class and its methods

The org.junit.jupiter.api.Test annotation denotes a test method. Note that the @Test annotation now comes from the JUnit 5 Jupiter API package instead of JUnit 4's org.junit package. The testConvertToDecimalSuccess method first executes the MathTools::convertToDecimal method with a numerator of 3 and a denominator of 4, then asserts that the result is equal to 0.75. The org.junit.jupiter.api.Assertions class provides a set of static methods for comparing actual and expected results. The Assertions class has the following methods, which cover most of the primitive data types:

  • assertArrayEquals compares the contents of an actual array to an expected array.
  • assertEquals compares an actual value to an expected value.
  • assertNotEquals compares two values to validate that they are not equal.
  • assertTrue validates that the provided value is true.
  • assertFalse validates that the provided value is false.
  • assertLinesMatch compares two lists of 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 е изпълнението на тестовия двигател, който изпълнява модулните тестове.
  • junit-jupiter-params осигурява поддръжка за параметризирани тестове.

След това трябва да добавим maven-surefire-pluginприставката за изграждане, за да стартираме тестовете.

И накрая, не забравяйте да включите maven-compiler-pluginверсията на Java 8 или по-нова, за да можете да използвате функции на Java 8 като ламбда.

Пусни го!

Използвайте следната команда, за да стартирате тестовия клас от вашата IDE или от Maven:

mvn clean test

Ако успеете, трябва да видите изход, подобен на следния:

 [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] ------------------------------------------------------------------------