Basic Java Tools for Building & Testing Apps
Скачать 4.76 Mb.
|
@Test @EnabledIf ( "customCondition" ) void enabled () { // ... } @Test @DisabledIf ( "customCondition" ) void disabled () { // ... } boolean customCondition () { return true; } Parameterized Tests Parameterized tests make it possible to run a test multiple times with different arguments. They are declared just like regular @Test methods but use the @ParameterizedTest annotation instead. In addition, you must declare at least one source that will provide the arguments for each invocation and then consume the arguments in the test method. The following example demonstrates a parameterized test that uses the @ValueSource annotation to specify a String array as the source of arguments. @ParameterizedTest @ValueSource (strings = { "racecar" , "radar" , "able was I ere I saw elba" }) void palindromes ( String candidate) { assertTrue( StringUtils . isPalindrome (candidate)); } When executing the above parameterized test method, each invocation will be reported separately. For instance, the ConsoleLauncher will print output similar to the following. palindromes(String) ✔ ├─ [1] candidate=racecar ✔ ├─ [2] candidate=radar ✔ └─ [3] candidate=able was I ere I saw elba ✔ Parameterized tests are currently an experimental feature. Consult the table in Experimental APIs for details. 2.15.1. Required Setup In order to use parameterized tests you need to add a dependency on the junit-jupiter-params artifact. Please refer to Dependency Metadata for details. 2.15.2. Consuming Arguments Parameterized test methods typically consume arguments directly from the configured source (see Sources of Arguments ) following a one-to- one correlation between argument source index and method parameter index (see examples in @CsvSource ). However, a parameterized test method may also choose to aggregate arguments from the source into a single object passed to the method (see Argument Aggregation ). Additional arguments may also be provided by a ParameterResolver (e.g., to obtain an instance of TestInfo , TestReporter , etc.). Specifically, a parameterized test method must declare formal parameters according to the following rules. Zero or more indexed arguments must be declared first. Zero or more aggregators must be declared next. Zero or more arguments supplied by a ParameterResolver must be declared last. In this context, an indexed argument is an argument for a given index in the Arguments provided by an ArgumentsProvider that is passed as an argument to the parameterized method at the same index in the method’s formal parameter list. An aggregator is any parameter of type ArgumentsAccessor or any parameter annotated with @AggregateWith 2.15.3. Sources of Arguments Out of the box, JUnit Jupiter provides quite a few source annotations. Each of the following subsections provides a brief overview and an example for each of them. Please refer to the Javadoc in the org.junit.jupiter.params.provider package for additional information. @ValueSource @ValueSource is one of the simplest possible sources. It lets you specify a single array of literal values and can only be used for providing a single argument per parameterized test invocation. The following types of literal values are supported by @ValueSource short byte int long float double char boolean java.lang.String java.lang.Class For example, the following @ParameterizedTest method will be invoked three times, with the values 1 , 2 , and 3 respectively. @ParameterizedTest @ValueSource (ints = { 1 , 2 , 3 }) void testWithValueSource ( int argument) { assertTrue(argument > 0 && argument < 4 ); } Null and Empty Sources In order to check corner cases and verify proper behavior of our software when it is supplied bad input, it can be useful to have null and empty values supplied to our parameterized tests. The following annotations serve as sources of null and empty values for parameterized tests that accept a single argument. @NullSource : provides a single null argument to the annotated @ParameterizedTest method. o @NullSource cannot be used for a parameter that has a primitive type. @EmptySource : provides a single empty argument to the annotated @ParameterizedTest method for parameters of the following types: java.lang.String , java.util.List , java.util.Set , java.util.Map , primitive arrays (e.g., int[] , char[][] , etc.), object arrays (e.g., String[] , Integer[][] , etc.). o Subtypes of the supported types are not supported. @NullAndEmptySource : a composed annotation that combines the functionality of @NullSource and @EmptySource If you need to supply multiple varying types of blank strings to a parameterized test, you can achieve that using @ValueSource — for example, @ValueSource(strings = {" ", " ", "\t", "\n"}) You can also combine @NullSource , @EmptySource , and @ValueSource to test a wider range of null , empty, and blank input. The following example demonstrates how to achieve this for strings. @ParameterizedTest @NullSource @EmptySource @ValueSource (strings = { " " , " " , "\t" , "\n" }) void nullEmptyAndBlankStrings ( String text) { assertTrue(text == null || text. trim (). isEmpty ()); } Making use of the composed @NullAndEmptySource annotation simplifies the above as follows. @ParameterizedTest @NullAndEmptySource @ValueSource (strings = { " " , " " , "\t" , "\n" }) void nullEmptyAndBlankStrings ( String text) { assertTrue(text == null || text. trim (). isEmpty ()); } Both variants of the nullEmptyAndBlankStrings(String) parameterized test method result in six invocations: 1 for null , 1 for the empty string, and 4 for the explicit blank strings supplied via @ValueSource @EnumSource @EnumSource provides a convenient way to use Enum constants. @ParameterizedTest @EnumSource ( ChronoUnit . class ) void testWithEnumSource ( TemporalUnit unit) { assertNotNull(unit); } The annotation’s value attribute is optional. When omitted, the declared type of the first method parameter is used. The test will fail if it does not reference an enum type. Thus, the value attribute is required in the above example because the method parameter is declared as TemporalUnit , i.e. the interface implemented by ChronoUnit , which isn’t an enum type. Changing the method parameter type to ChronoUnit allows you to omit the explicit enum type from the annotation as follows. @ParameterizedTest @EnumSource void testWithEnumSourceWithAutoDetection ( ChronoUnit unit) { assertNotNull(unit); } The annotation provides an optional names attribute that lets you specify which constants shall be used, like in the following example. If omitted, all constants will be used. @ParameterizedTest @EnumSource (names = { "DAYS" , "HOURS" }) void testWithEnumSourceInclude ( ChronoUnit unit) { assertTrue( EnumSet . of ( ChronoUnit . DAYS , ChronoUnit . HOURS ). contains (unit)); } The @EnumSource annotation also provides an optional mode attribute that enables fine-grained control over which constants are passed to the test method. For example, you can exclude names from the enum constant pool or specify regular expressions as in the following examples. @ParameterizedTest @EnumSource (mode = EXCLUDE , names = { "ERAS" , "FOREVER" }) void testWithEnumSourceExclude ( ChronoUnit unit) { assertFalse( EnumSet . of ( ChronoUnit . ERAS , ChronoUnit . FOREVER ). contains (unit)); } @ParameterizedTest @EnumSource (mode = MATCH_ALL , names = "^.*DAYS$" ) void testWithEnumSourceRegex ( ChronoUnit unit) { assertTrue(unit. name (). endsWith ( "DAYS" )); } @MethodSource @MethodSource allows you to refer to one or more factory methods of the test class or external classes. Factory methods within the test class must be static unless the test class is annotated with @TestInstance(Lifecycle.PER_CLASS) ; whereas, factory methods in external classes must always be static . In addition, such factory methods must not accept any arguments. Each factory method must generate a stream of arguments, and each set of arguments within the stream will be provided as the physical arguments for individual invocations of the annotated @ParameterizedTest method. Generally speaking this translates to a Stream of Arguments (i.e., Stream ); however, the actual concrete return type can take on many forms. In this context, a "stream" is anything that JUnit can reliably convert into a Stream , such as Stream , DoubleStream , LongStream , IntStream , Collection , Iterator , Iterable , an array of objects, or an array of primitives. The "arguments" within the stream can be supplied as an instance of Arguments , an array of objects (e.g., Object[] ), or a single value if the parameterized test method accepts a single argument. If you only need a single parameter, you can return a Stream of instances of the parameter type as demonstrated in the following example. @ParameterizedTest @MethodSource ( "stringProvider" ) void testWithExplicitLocalMethodSource ( String argument) { assertNotNull(argument); } static Stream < String > stringProvider () { return Stream . of ( "apple" , "banana" ); } If you do not explicitly provide a factory method name via @MethodSource , JUnit Jupiter will search for a factory method that has the same name as the current @ParameterizedTest method by convention. This is demonstrated in the following example. @ParameterizedTest @MethodSource void testWithDefaultLocalMethodSource ( String argument) { assertNotNull(argument); } static Stream < String > testWithDefaultLocalMethodSource () { return Stream . of ( "apple" , "banana" ); } Streams for primitive types ( DoubleStream , IntStream , and LongStream ) are also supported as demonstrated by the following example. @ParameterizedTest @MethodSource ( "range" ) void testWithRangeMethodSource ( int argument) { assertNotEquals( 9 , argument); } static IntStream range () { return IntStream . range ( 0 , 20 ). skip ( 10 ); } If a parameterized test method declares multiple parameters, you need to return a collection, stream, or array of Arguments instances or object arrays as shown below (see the Javadoc for @MethodSource for further details on supported return types). Note that arguments(Object…) is a static factory method defined in the Arguments interface. In addition, Arguments.of(Object…) may be used as an alternative to arguments(Object…) @ParameterizedTest @MethodSource ( "stringIntAndListProvider" ) void testWithMultiArgMethodSource ( String str, int num, List < String > list) { assertEquals( 5 , str. length ()); assertTrue(num >= 1 && num <= 2 ); assertEquals( 2 , list. size ()); } static Stream < Arguments > stringIntAndListProvider () { return Stream . of ( arguments( "apple" , 1 , Arrays . asList ( "a" , "b" )), arguments( "lemon" , 2 , Arrays . asList ( "x" , "y" )) ); } An external, static factory method can be referenced by providing its fully qualified method name as demonstrated in the following example. package example ; import java.util.stream.Stream ; import org.junit.jupiter.params.ParameterizedTest ; import org.junit.jupiter.params.provider.MethodSource ; class ExternalMethodSourceDemo { @ParameterizedTest @MethodSource ( "example.StringsProviders#tinyStrings" ) void testWithExternalMethodSource ( String tinyString) { // test with tiny string } } class StringsProviders { static Stream < String > tinyStrings () { return Stream . of ( "." , "oo" , "OOO" ); } } Передовой опыт тестирования в Java Автор оригинала: Philipp Hauer Блог компании FunCorp , Java , Тестирование веб-сервисов , Kotlin Перевод Чтобы покрытие кода было достаточным, а создание нового функционала и рефакторинг старого проходили без страха что-то сломать, тесты должны быть поддерживаемыми и легко читаемыми. В этой статье я расскажу о множестве приёмов написания юнит- и интеграционных тестов на Java, собранных мной за несколько лет. Я буду опираться на современные технологии: JUnit5, AssertJ, Testcontainers, а также не обойду вниманием Kotlin. Некоторые советы покажутся вам очевидными, другие могут идти вразрез с тем, что вы читали в книгах о разработке ПО и тестировании. Вкратце Пишите тесты кратко и конкретно, используя вспомогательные функции, параметризацию, разнообразные примитивы библиотеки AssertJ, не злоупотребляйте переменными, проверяйте только то, что относится к тестируемому функционалу и не засовывайте все нестандартные случаи в один тест Пишите самодостаточные тесты, раскрывайте все релевантные параметры, вставляйте тестовые данные прямо внутрь тестов и вместо наследования пользуйтесь композицией Пишите прямолинейные тесты, чтобы не переиспользовать продакшн-код, сравнивайте выдачу тестируемых методов с константами прямо в коде теста KISS важнее DRY Запускайте тесты в среде, максимально похожей на боевую, тестируйте максимально полную связку компонентов, не используйте in-memory-базы данных JUnit5 и AssertJ — очень хороший выбор Вкладывайтесь в простоту тестирования: избегайте статических свойств и методов, используйте внедрение в конструкторы, используйте экземпляры класса Clock и отделяйте бизнес-логику от асинхронной. Общие положения Given, When, Then (Дано, Когда, То) Тест должен содержать три блока, разделённых пустыми строками. Каждый блок должен быть максимально коротким. Используйте локальные методы для компактности записи. Given / Дано (ввод): подготовка теста, например, создание данных и конфигурация моков. When / Когда (действие): вызов тестируемого метода Then / То (вывод): проверка корректности полученного значения |