From JUnit in Action, Third Edition by Catalin Tudose

This article discusses dependency injection and testing in Junit 5.

Take 37% off JUnit in Action, Third Edition. Just enter fcctudose into the discount box at checkout at manning.com.

 Dependency injection in JUnit 5

In all prior JUnit versions, test constructors or methods weren’t allowed to have parameters. As one of the major changes in JUnit 5, both test constructors and methods are now permitted to have parameters. This allows for greater flexibility and enables dependency injection for constructors and methods.

ParameterResolver defines the API for test extensions that wish to dynamically resolve parameters at runtime. If a test class constructor, a test method, or a lifecycle method accepts a parameter, the parameter must be resolved at runtime by a registered ParameterResolver. You can inject as many parameters as you want in any order you want them to be.

Although there are currently three built-in resolvers, other parameter resolvers must be explicitly enabled by registering appropriate extensions via @ExtendWith. The automatically registered parameter resolvers are:

  1. TestInfoParameterResolver: if a constructor or method parameter is of type TestInfo, the TestInfoParameterResolver supplies an instance of TestInfo corresponding to the current container or test as the value for the parameter. TestInfo is a class whose objects are used to inject information about the currently executed test or container into to @Test, @BeforeEach, @AfterEach, @BeforeAll, and @AfterAll methods. The TestInfo can then be used to retrieve information about the current container or test such as the display name, the test class, the test method, and associated tags. The display name is either a technical name, such as the name of the test class or test method, or a custom name configured via @DisplayName. Listing 12 shows how to use a TestInfo parameter as argument of a constructor and of annotated methods.

Listing 12 The usage of TestInfo parameters

  
 class TestInfoTest {
     TestInfoTest(TestInfo testInfo) {
         assertEquals("TestInfoTest", testInfo.getDisplayName());           (1)
     }
  
     @BeforeEach
     void setUp(TestInfo testInfo) {
         String displayName = testInfo.getDisplayName();
         assertTrue(displayName.equals("display name of the method") ||     (2)
                    displayName.equals("testGetNameOfTheMethod(TestInfo)"));(2)
     }
  
     @Test
     void testGetNameOfTheMethod(TestInfo testInfo) {
         assertEquals("testGetNameOfTheMethod(TestInfo)",
                      testInfo.getDisplayName());                           (3)
     }
  
     @Test
     @DisplayName("display name of the method")
     void testGetNameOfTheMethodWithDisplayNameAnnotation(TestInfo testInfo) {
         assertEquals("display name of the method",
                      testInfo.getDisplayName());                           (4)
     }
 }
  

Into the previous example, a TestInfo parameter is injected into the constructor and into three methods. The constructor verifies that the display name is exactly TestInfoTest, its own name (1). This is the default behavior which you may vary with the help of the @DisplayName annotations, as you’ll see immediately.

Then, the @BeforeEach annotated method is executed before each test. It has an injected TestInfo parameter and it verifies that the displayed name is the expected one, meaning either the name of the method or the one specified by the @DisplayName annotation (2).

Both tests have an injected TestInfo parameter. Each of them verifies that the displayed name is the expected one, meaning either the name of the method for the first test (3) or the one specified by the @DisplayName annotation for the second test (4).

We remind that the built-in TestInfoParameterResolver supplies an instance of TestInfo corresponding to the current container or test as the value for the expected parameters of the constructor and of the methods.

  1. TestReporterParameterResolver: if a constructor or method parameter is of type TestReporter, the TestReporterParameterResolver supplies an instance of TestReporter. TestReporter is a functional interface and can therefore be used as the assignment target for a lambda expression or method reference. Parameters of type TestReporter can be injected into methods of test classes annotated with @BeforeEach, @AfterEach, and @Test. The TestReporter can be used to publish additional data about the current test run. Listing 13 shows how to use use a TestReporter parameter as argument of @Test annotated methods.

Listing 13 The usage of TestReporter parameters

  
 class TestReporterTest {
  
     @Test
     void testReportSingleValue(TestReporter testReporter) {
         testReporter.publishEntry("Single value");                         (1)
     }
  
     @Test
     void testReportKeyValuePair(TestReporter testReporter) {
         testReporter.publishEntry("Key", "Value");                         (2)
     }
  
     @Test
     void testReportMultipleKeyValuePairs(TestReporter testReporter) {
         Map<String, String> values = new HashMap<>();                      (3)
         values.put("user", "John");                                        (4)
         values.put("password", "secret");                                  (4)
  
         testReporter.publishEntry(values);                                 (5)
     }
  
 }
  

Into the previous example, a TestReporter parameter is injected into three methods.

Into the first method, it’s used for publishing a single value entry (1).

Into the second method, it’s used for publishing a key-value pair (2).

Into the third method, we first construct a map (3), then populate it with two key-values pairs (4), then we use it for publishing the constructed map (5).

We remind that the built-in TestReporterParameterResolver supplies the instance of TestReporter needed for publishing the entries.

The result of the execution of this test is shown in figure 3.


Figure 3 The result of the execution of TestReporterTest


  1. RepetitionInfoParameterResolver: if a method parameter in a @RepeatedTest, @BeforeEach, or @AfterEach method is of type RepetitionInfo, the RepetitionInfoParameterResolver supplies an instance of RepetitionInfo. RepetitionInfo can then be used to retrieve information about the current repetition and the total number of repetitions for the corresponding @RepeatedTest. RepetitionInfoParameterResolver isn’t registered outside the context of a @RepeatedTest. We’ll discuss more about repeated tests and provide examples into the next section.

Repeated tests

JUnit 5 provides the ability to repeat a test a specified number of times by annotating a method with @RepeatedTest and specifying the total number of repetitions desired. This may be particularly useful when some conditions may change from one execution of a test to another one. Each invocation of a repeated test behaves like the execution of a regular @Test method with full support for the lifecycle callbacks and extensions.

In addition to specifying the number of repetitions, a custom display name can be configured for each repetition via the name attribute of the @RepeatedTest annotation. The following placeholders are currently supported:

  • {displayName}: display name of the @RepeatedTest method
  • {currentRepetition}: current repetition number
  • {totalRepetitions}: total number of repetitions

Listing 14 shows the usage of repeated tests, of the display name placeholders and of the RepetitionInfo parameters. The scenario of the first repeated test verifies that the execution of the add method from the Calculator class is stable and it always provides the same result. The scenario of the second repeated test verifies that collections follow the appropriate behavior: a list is receiving a new element at each iteration, but a set will not get duplicate elements, even if we try to insert it a few times.

Listing 14 The usage of repeated tests

  
 public class RepeatedTestsTest {
  
     private static Set<Integer> integerSet = new HashSet<>();
     private static List<Integer> integerList = new ArrayList<>();
  
     @RepeatedTest(value = 5, name =                                        (1)
 "{displayName} - repetition {currentRepetition} of {totalRepetitions}")    (1)
     @DisplayName("Test add operation")
     void addNumber() {
         Calculator calculator = new Calculator();
         assertEquals(2, calculator.add(1, 1),
                      "1 + 1 should equal 2");
     }
  
     @RepeatedTest(value = 5, name = "the list contains                     (2)
 {currentRepetition} elements(s), the set contains 1 element")              (2)
     void testAddingToCollections(TestReporter testReporter,
                                  RepetitionInfo repetitionInfo) {
         integerSet.add(1);
         integerList.add(repetitionInfo.getCurrentRepetition());
  
         testReporter.publishEntry("Repetition number",                     (3)
             String.valueOf(repetitionInfo.getCurrentRepetition()));        (3)
         assertEquals(1, integerSet.size());
         assertEquals(repetitionInfo.getCurrentRepetition(),  
                      integerList.size());
     }
 }
  

For the example above, we remark the following:

  1. The first test is repeated five times, and at each repetition it shows the display name, the current repetition number and the total number of repetitions (1).
  2. The second test is repeated five times, and at each repetition it shows the number of elements into the list (which is the current repetition number) and the fact that the set always has only one element (2).
  3. At the execution of each of the repeated second test, the repetition number is displayed, as it’s injected into the RepetitionInfo parameter (3).

The result of the execution of the previous tests is shown in figures 4 and 5.


Figure 4 The names of the repeated tests at the time of the execution



Figure 5 The messages shown into the console by the second repeated test


Parameterized tests

Parameterized tests make possible the running of a test multiple times with different arguments. The great benefit is that you can write one single test, and the testing is done on a series of different arguments. Your tests will be safer and check various input data. The methods are annotated using @ParameterizedTest. You must declare at least one source, which provides the arguments for each invocation and then consumes the arguments in the test method.

@ValueSource is the simplest of the possible sources. You must specify a single array of literal values for providing a single argument per parameterized test invocation. The usage of this annotation is shown into listing 15. The scenario of the test intends to check the number of words in some given sentences, provided as parameters.

Listing 15 The usage of the @ValueSource annotation

  
 class ParameterizedWithValueSourceTest {
     private WordCounter wordCounter = new WordCounter();
  
     @ParameterizedTest                                                     (1)
     @ValueSource(strings = {"Check three parameters",                      (2)
                             "JUnit in Action"})                            (2)
     void testWordsInSentence(String sentence) {
         assertEquals(3, wordCounter.countWords(sentence));
     }
 }
  

Into the previous example, we do the following:

  1. We mark the test as a parameterized one using the corresponding annotation (1).
  2. Then, we specify the values to be passed as argument of the testing method (2). The testing method is executed twice, once for each of the arguments provided by the @ValueSource annotation.

@EnumSource provides the possibility to use enum instances. The annotation provides an optional names parameter which allows you to specify which of the instances must be used or excluded. By default, all instances of an enum are used.

Listing 16 shows the usage of the @EnumSource annotation to check the number of words in some given sentences, provided as enum instances.

Listing 16 The usage of the @EnumSource annotation

  
 class ParameterizedWithEnumSourceTest {
     private WordCounter wordCounter = new WordCounter();
  
     @ParameterizedTest                                                     (1)
     @EnumSource(Sentences.class)                                           (1)
     void testWordsInSentence(Sentences sentence) {
         assertEquals(3, wordCounter.countWords(sentence.value()));
     }
  
     @ParameterizedTest                                                     (2)
     @EnumSource(value=Sentences.class,                                     (2)
                 names = { "JUNIT_IN_ACTION", "THREE_PARAMETERS" })         (2)
     void testSelectedWordsInSentence(Sentences sentence) {
         assertEquals(3, wordCounter.countWords(sentence.value()));
    }
  
     @ParameterizedTest                                                     (3)
     @EnumSource(value=Sentences.class, mode = EXCLUDE, names =             (3)
                 { "THREE_PARAMETERS" })                                    (3)
     void testExcludedWordsInSentence(Sentences sentence) {
         assertEquals(3, wordCounter.countWords(sentence.value()));
     }
  
     enum Sentences {
         JUNIT_IN_ACTION("JUnit in Action"),
         SOME_PARAMETERS("Check some parameters"),
         THREE_PARAMETERS("Check three parameters");
  
         private final String sentence;
  
         Sentences(String sentence) {
             this.sentence = sentence;
         }
  
         public String value() {
             return sentence;
         }
     }
 }
  

Into the previous example we’ve one parameterized test. The functionality is as follows:

  1. The test receives as parameters a CSV as specified into the @CsvSource annotation (1). This test is executed three times, once for each of the CSV lines.
  2. The CSV line is parsed and the first value is assigned to the expected parameter, and the second value is assigned to the sentence parameter.

@CsvFileSource allows you to use CSV files from the classpath. Each line from a CSV file triggers one execution of the parameterized test.

Listing 18 shows the usage of the @CsvFileSource annotation, and listing 19 displays the content of the word_counter.csv file on the classpath. The scenario of the test intends to check the number of words in some given sentences, provided as parameters—this time, in CSV format, having a CSV file as resource input.

Listing 18 The usage of the @CsvFileSource annotation

  
 class ParameterizedWithCsvFileSourceTest {
     private WordCounter wordCounter = new WordCounter();
  
     @ParameterizedTest                                                     (1)
     @CsvFileSource(resources = "/word_counter.csv")                        (1)
     void testWordsInSentence(int expected, String sentence) {
         assertEquals(expected, wordCounter.countWords(sentence));
     }
 }
  

Listing 19 The content of the word_counter.csv file

 2, Unit testing
 3, JUnit in Action
 4, Write solid Java code

Into the previous example we’ve one parameterized test. It receives as parameters the ones specified into the file indicated into the @CsvFileSource annotation (1). This test is executed three times, once for each of the CSV file lines. The CSV file line is parsed and the first value is assigned to the expected parameter, and the second value is assigned to the sentence parameter.

That’s all for this article.

If you want to learn more about the book, check it out on our browser-based liveBook reader here and see this slide deck.