Contents


Introducing JUnit 5, Part 2

JUnit 5 Vintage and the JUnit Jupiter Extension Model

Get started with JUnit Jupiter extensions for parameter injection, paramaterized tests, dynamic tests, and custom annotations

Comments

Content series:

This content is part # of # in the series: Introducing JUnit 5, Part 2

Stay tuned for additional content in this series.

This content is part of the series:Introducing JUnit 5, Part 2

Stay tuned for additional content in this series.

In Part 1 of this tutorial, I introduced you to JUnit 5, including setup instructions and a tour of JUnit 5's architecture and components. I also showed you how to use new features in the JUnit Jupiter API, including annotations, assertions, and assumptions.

In this part, you'll become familiar with two other modules that comprise the new JUnit 5: JUnit Vintage and the JUnit Jupiter Extension Model. I'll show you how to use these components for things like parameter injection, paramaterized tests, dynamic tests, and custom annotations.

Just like in Part 1, I'll show you how to run your tests using both Maven and Gradle.

Note that examples for this tutorial are based on JUnit 5, Version 5.0.2.

Prerequisites

I assume that you are comfortable using the following software:

  • Eclipse IDE
  • Maven
  • Gradle (optional)
  • Git

In order to follow along with the examples, you should have JDK 8, Eclipse, Maven, Gradle (optional), and Git installed on your computer. If you're missing any of these tools, you can use the links below to download and install them now:

JUnit Vintage

It's always a risk to upgrade to a major new software release, but in this case upgrading is not only a good idea, but a safe one.

Because many organizations are significantly invested in JUnit 4 (and even in JUnit 3), JUnit 5's development team created the JUnit Vintage package, which includes the JUnit Vintage test engine. JUnit Vintage ensures that existing JUnit tests can run alongside newer tests created using JUnit Jupiter.

JUnit 5's architecture also supports running multiple test engines simultaneously: you can run the JUnit Vintage test engine with virtually any other test engine that is compatible with JUnit 5.

Now that you know about JUnit Vintage, you might be wondering how it works. Figure 1 shows the JUnit 5 dependency diagram from Part 1, illustrating the relationship between the various packages in JUnit 5.

Figure 1. JUnit 5 dependency diagram
An illustrated JUnit 5 dependency diagram.
An illustrated JUnit 5 dependency diagram.

JUnit Vintage, shown in the middle row of Figure 1, is intended to provide a "gentle migration path" to JUnit Jupiter. Two JUnit 5 modules depend on JUnit Vintage:

  • junit-platform-runner provides a Runner to execute tests in a JUnit 4 environment such as Eclipse.
  • junit-jupiter-migration-support supports backward compatibility to select JUnit 4 Rules.

JUnit Vintage itself is comprised of two modules:

  • junit:junit is the API for JUnit 3 and JUnit 4.
  • junit-vintage-engine is the test engine for running JUnit 3 and JUnit 4 tests on the JUnit Platform.

Because the JUnit Platform allows multiple test engines to run simultaneously, you can run your JUnit 3 and JUnit 4 tests side-by-side with tests written using JUnit Jupiter. I'll show you how to do that later in the tutorial.

Before we get into running tests in Eclipse, Maven, and Gradle, let's take a moment to recap what a basic unit test looks like. We'll look at tests written in both JUnit 3 and JUnit 4.

Tests in JUnit 3

Tests written using JUnit 3 will run as-is on the JUnit Platform. Simply include the junit-vintage dependency in your build, and the rest just works.

In the sample application, you'll see Maven POM (pom.xml) and Gradle build files (build.gradle) that are part of the sample application already, so you can run these tests right out of the box.

Listing 1 shows part of one of the JUnit 3 tests from the sample application. It is located in the src/test/java tree, in the com.makotojava.learn.junit3 package.

Listing 1. JUnit 3 test case from the HelloJunit5Part2 sample application
.
.
public class PersonDaoBeanTest extends TestCase {

  private ApplicationContext ctx;

  private PersonDaoBean classUnderTest;

  @Override
  protected void setUp() throws Exception {
    ctx = new AnnotationConfigApplicationContext(TestSpringConfiguration.class);
    classUnderTest = ctx.getBean(PersonDaoBean.class);
  }

  @Override
  protected void tearDown() throws Exception {
    DataSource dataSource = (DataSource) ctx.getBean("dataSource");
    if (dataSource instanceof EmbeddedDatabase) {
      ((EmbeddedDatabase) dataSource).shutdown();
    }
  }

  public void testFindAll() {
    assertNotNull(classUnderTest);
    List<Person> people = classUnderTest.findAll();
    assertNotNull(people);
    assertFalse(people.isEmpty());
    assertEquals(5, people.size());
  }
.
.
}

A JUnit 3 test case extends the JUnit 3 API class TestCase (line 3) and each test method must start with the word test (line 23).

To run this test in Eclipse, right-click on the test class in the Package Explorer view, and choose Run As > Junit Test.

I'll show you how to run this test using Maven and Gradle later in the tutorial.

Tests in JUnit 4

Your JUnit 4 tests run as-is on the JUnit Platform. Simply include the junit-vintage dependency in your build, and it just works.

The Maven POM and the Gradle build file (build.gradle) that are included with the sample application already do this, so you can run these tests right out of the box.

Listing 1 shows part of one of the JUnit 4 tests from the sample application. It is located in the src/test/java tree, in the com.makotojava.learn.junit4 package.

Listing 2. JUnit 4 test case from the HelloJunit5Part2 sample application
.
.
public class PersonDaoBeanTest {

  private ApplicationContext ctx;

  private PersonDaoBean classUnderTest;

  @Before
  public void setUp() throws Exception {
    ctx = new AnnotationConfigApplicationContext(TestSpringConfiguration.class);
    classUnderTest = ctx.getBean(PersonDaoBean.class);
  }

  @After
  public void tearDown() throws Exception {
    DataSource dataSource = (DataSource) ctx.getBean("dataSource");
    if (dataSource instanceof EmbeddedDatabase) {
      ((EmbeddedDatabase) dataSource).shutdown();
    }
  }

  @Test
  public void findAll() {
    assertNotNull(classUnderTest);
    List<Person> people = classUnderTest.findAll();
    assertNotNull(people);
    assertFalse(people.isEmpty());
    assertEquals(5, people.size());
  }
.
.
}

A JUnit 4 test case ends with the word Test (line 3) and each test method is annotated with @Test (line 23).

To run this test in Eclipse, right-click on the test class in the Package Explorer view, and choose Run As > Junit Test.

I'll show you how to run this test using Maven and Gradle later in the tutorial.

Migration support for JUnit Jupiter

The junit-jupiter-migration-support package contains select Rules for backward compatibility, in case you are heavily invested in JUnit 4 rules. In JUnit 5, you will use the JUnit Jupiter Extension Model to implement the same behavior provided by rules in JUnit 4. I'll show you how to do that in the next section.

The JUnit Jupiter Extension Model

Using the JUnit Extension Model, it's now possible for any developer or tool vendor to extend the core functionality of JUnit.

To truly appreciate how groundbreaking the JUnit Jupiter Extension Model is, you need to understand how it extends the core functionality of JUnit 4. If you understand this already, feel free to skip the next section.

Extending JUnit 4's core functionality

In the past, developers or tool vendors who wanted to extend the core functionality of JUnit 4 used Runners and @Rules.

A Runner is typically a subclass of BlockJUnit4ClassRunner, which is used to provide some kind of behavior not part of JUnit out of the box. A number of third-party Runners exist, such as SpringJUnit4ClassRunner for running Spring-based unit tests, and MockitoJUnitRunner for working with Mockito objects in unit tests.

A Runner must be declared at the test-class level, using the @RunWith annotation. @RunWith takes a single parameter: the Runner's implementation class. Because each test class can have at most one Runner, each test class can have at most one extension point.

To address this built-in limitation of the Runner concept, JUnit 4.7 introduced @Rules. A test class may declare multiple @Rules, which can work at both the test-method level and the class level (not just at the class level, as with a Runner).

Given that JUnit 4.7's @Rules workaround handles most situations nicely, you might wonder why we need the new JUnit Jupiter Extension Model. I'll explain that in the next section.

Features vs extensions

One of JUnit 5's core principles is to prefer extension points to features.

This means that while JUnit could provide features for tool vendors and developers, the JUnit 5 team prefer to provide extension points in the architecture. This enables third parties (whether tool vendors, test writers, or whoever) to write extensions at those points. There are three reasons for preferring extension points, as the JUnit Wiki explains:

  • JUnit is not an all-inclusive utility, nor does it try to be.
  • Third-party developers know what they need and can write code to meet that need more quickly than the JUnit team could respond with a feature request.
  • APIs are hard to change once they have been released.

Next I'll explain how to extend the JUnit Jupiter API, starting with the extension points.

Extension points and the test lifecycle

An extension point corresponds to a predefined point in the JUnit test lifecycle. In Java™ language terms, an extension point is a callback interface that you implement and then register (activate) with JUnit. Thus, the extension point is the callback interface, and the extension is the implementation of that interface.

In this tutorial, I will refer to an implemented extension point callback interface as an extension.

Once registered, your extension is activated. JUnit will use the callback interface to invoke it at the appropriate point in the test lifecycle.

Table 1 summarizes the extension points in the JUnit Jupiter Extension Model.

Table 1. Extension points
InterfaceDescription
AfterAllCallbackDefines the API for extensions that wish to provide additional behavior to test containers after all tests have been invoked.
AfterEachCallbackDefines the API for extensions that wish to provide additional behavior to tests after each test method has been invoked.
AfterTestExecutionCallbackDefines the API for extensions that wish to provide additional behavior to tests immediately after each test has been executed.
BeforeAllCallbackDefines the API for extensions that wish to provide additional behavior to test containers before all tests are invoked.
BeforeEachCallbackDefines the API for extensions that wish to provide additional behavior to tests before each test is invoked.
BeforeTestExecutionCallbackDefines the API for extensions that wish to provide additional behavior to tests immediately before each test is executed.
ParameterResolverDefines the API for extensions that wish to dynamically resolve parameters at runtime.
TestExecutionExceptionHandlerDefines the API for extensions that wish to handle exceptions thrown during test execution.

The extension point callback interfaces listed in Table 1 are implemented in the sample application's JUnit5ExtensionShowcase class. You can find that class in the test/src tree, in the com.makotojava.learn.junit5 package.

Creating an extension

To create an extension you simply implement the extension point's callback interface. Suppose I wanted to create an extension that runs before each test method runs. In this case I'd just need to implement the BeforeEachCallback interface:

                public class MyBeforeEachCallbackExtension implements BeforeEachCallback {
                  @Override
                  public void beforeEach(ExtensionContext context) throws Exception {
                    // Implementation goes here
                  }
                }

Once the extension point interface has been implemented, you need to activate it, so that JUnit can invoke it at the appropriate point in the test lifecycle. You activate the extension by registering it.

Activating an extension

To activate the extension from above, simply register it using the @ExtendWith annotation:

                @ExtendWith(MyBeforeEachCallbackExtension.class)
                public class MyTestClass {
                .
                .
                    @Test
                    public void myTestMethod() {
                        // Test code here
                    }
                    @Test
                    public void someOtherTestMethod() {
                        // Test code here
                    }
                .
                .
                }

When MyTestClass runs, before each @Test method is executed the MyBeforeEachCallbackExtension will be invoked.

Note that this style of registering extensions is declarative. JUnit also provides an automatic registration mechanism, using Java's ServiceLoader mechanism. I won't provide details here, but there is plenty of good information available in the extension model section of the JUnit 5 User Guide.

Parameter injection

Let's suppose you wanted to pass a parameter to a @Test method. How would you go about doing it? That's what you'll learn next.

The ParameterResolver interface

When you write a test method that contains a parameter in its signature, the parameter must be resolved to an actual object before JUnit can call the method. An optimistic scenario would be as follows: JUnit (1) looks for a registered extension that implements the ParameterResolver interface; (2) invokes it to resolve the parameter; and (3) invokes your test method, passing the resolved parameter value.

The ParameterResolver interface consists of two methods:

                package org.junit.jupiter.api.extension;
                
                import static org.junit.platform.commons.meta.API.Usage.Experimental;
                import java.lang.reflect.Parameter;
                import org.junit.platform.commons.meta.API;
                
                @API(Experimental)
                public interface ParameterResolver extends Extension {
                
                	boolean supportsParameter(ParameterContext parameterContext, 
                                              ExtensionContext extensionContext)
                			throws ParameterResolutionException;
                
                	Object resolveParameter(ParameterContext parameterContext, 
                                            ExtensionContext extensionContext)
                			throws ParameterResolutionException;
                
                }

When the Jupiter test engine needs to resolve a parameter in your test class, it first calls the supports() method to see if the extension can handle that parameter type. If supports() returns true, then the Jupiter test engine calls resolve() to get back an Object of the correct type, which it subsequently uses when calling the test method.

If no extension can be found to handle the parameter type, you'll see a message like this one:

org.junit.jupiter.api.extension.ParameterResolutionException: 
No ParameterResolver registered for parameter [java.lang.String arg0] in executable 
[public void com.makotojava.learn.junit5.PersonDaoBeanTest$WhenDatabaseIsPopulated.findAllByLastName(java.lang.String)].
.
.

Creating a ParameterResolver implementation

To create a ParameterResolver, you simply implement the interface:

Listing 3. ParameterResolver extension point implementation for Person objects
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolutionException;
import org.junit.jupiter.api.extension.ParameterResolver;

import com.makotojava.learn.junit.Person;
import com.makotojava.learn.junit.PersonGenerator;

public class GeneratedPersonParameterResolver implements ParameterResolver {

  @Override
  public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
      throws ParameterResolutionException {
    return parameterContext.getParameter().getType() == Person.class;
  }

  @Override
  public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
      throws ParameterResolutionException {
    return PersonGenerator.createPerson();
  }

}

In this particular case, if the parameter's type is Person (line 14) then supports() returns true. When JUnit needs to resolve the parameter to a Person object, it calls resolve(), which returns a new generated Person object (line 20).

Using a ParameterResolver implementation

In order to use a ParameterResolver, you must register it with the JUnit Jupiter test engine. You do this with the @ExtendWith annotation, as I previously demonstrated.

Listing 4. Using a ParameterResolver
@DisplayName("Testing PersonDaoBean")
@ExtendWith(GeneratedPersonParameterResolver.class)
public class PersonDaoBeanTest extends AbstractBaseTest {
.
.
    @Test
    @DisplayName("Add generated Person should succeed - uses Parameter injection")
    public void add(Person person) {
      assertNotNull(classUnderTest, "PersonDaoBean reference cannot be null.");
      Person personAdded = classUnderTest.add(person);
      assertNotNull(personAdded, "Add failed but should have succeeded");
      assertNotNull(personAdded.getId());
      performPersonAssertions(person.getLastName(), person.getFirstName(), person.getAge(), person.getEyeColor(),
          person.getGender(), personAdded);
    }
.
.
}

When the PersonDaoBeanTest class runs, it will register the GeneratedPersonParameterResolver with the Jupiter test engine. Each time a parameter needs to be resolved, the custom ParameterResolver will be invoked.

An extension has a scope of influence, which is either at the class level or the method level.

In this particular case, I chose to register the extension at the class level (line 2). Registering at the class level means that any test method that takes any parameter will cause JUnit to invoke the GeneratedPersonParameterResolver extension. If the parameter type is Person, a generated Person object is returned and passed to the test method (line 8).

To narrow the extension's scope to a single method, you would register the extension as shown:

Listing 5. Using a ParameterResolver for a single method only
    @Test
    @DisplayName("Add generated Person should succeed - uses Parameter injection")
    @ExtendWith(GeneratedPersonParameterResolver.class)
    public void add(Person person) {
      assertNotNull(classUnderTest, "PersonDaoBean reference cannot be null.");
      Person personAdded = classUnderTest.add(person);
      assertNotNull(personAdded, "Add failed but should have succeeded");
      assertNotNull(personAdded.getId());
      performPersonAssertions(person.getLastName(), person.getFirstName(), person.getAge(), person.getEyeColor(),
          person.getGender(), personAdded);
    }

Now the extension will only be invoked to resolve parameters for the add() test method. If any other test methods in the class need parameter resolution, they will require a different ParameterResolver.

Note that you may have only one ParameterResolver at a particular scope for any given class. As an example, if you had one ParameterResolver for Person objects declared at the class level, and another ParameterResolver within that same class for objects declared at the method level, JUnit would not know which one to use. You would get this message as a result, indicating the ambiguity:

org.junit.jupiter.api.extension.ParameterResolutionException: 
Discovered multiple competing ParameterResolvers for parameter 
[com.makotojava.learn.junit.Person arg0] in executable 
[public void com.makotojava.learn.junit5.PersonDaoBeanTest$WhenDatabaseIsPopulated.update(com.makotojava.learn.junit.Person)]: .
.
.

Ready for a video break?

In the next section you'll get started with parameterized tests, but first let's take a minute for some hands-on learning. The following video walks you through using ParameterResolver and the @ParameterizedTest annotation to test a Spring-based application in JUnit 5.

Parameterized tests

A parameterized test is one where the @Test method is invoked multiple times with different parameter values each time. A parameterized test must be annotated with @ParameterizedTest, and must specify a source for its arguments.

JUnit Jupiter provides several sources. Each source specifies an @ArgumentsSource, which is an implementation of ArgumentsProvider. In this section I'll show you how to use three sources:

  • @ValueSource
  • @EnumSource
  • @MethodSource

With each of these sources, there is a tradeoff between ease-of-use and flexibility of allowed data types. At one end, the easiest to use, but least flexible (limited to a subset of Java primitives) is @ValueSource. At the other end is @MethodSource, the most flexible, allowing you to parameterize your test methods with any complex object you choose. (Note that @MethodSource is also the most difficult to use.)

@ValueSource

A @ValueSource is one where you specify a single array of literals that will be supplied—one at-a-time—to your @ParameterizedTest method.

The syntax looks like this:

                @ParameterizedTest
                @ValueSource(longs = { 1L, 2L, 3L, 4L, 5L })
                public void findById(Long id) {
                  assertNotNull(classUnderTest);
                  Person personFound = classUnderTest.findById(id);
                  assertNotNull(personFound);
                  assertEquals(id, personFound.getId());
                }

First, you tell JUnit that the findById() method is a @ParameterizedTest as shown in line 1 above. Then you specify the array using array initializer syntax, as shown in line 2. JUnit will invoke the findById() test method, each time passing the next long from the array to the method (line 3), until the array is exhausted. You use the parameter just as you would any Java method parameter (line 5).

The @ValueSource attribute name you supply as the array name must be all lowercase, and it must match its type with the letter s at the end. For example, ints for an array of int, strings for an array of Strings, and so forth.

Not all primitive types are supported, only these:

  • String
  • int
  • long
  • double

@EnumSource

An @EnumSource is one where you specify an enum that JUnit will supply—one at-a-time—to your @ParameterizedTest method.

The syntax looks like this:

                @ParameterizedTest
                @EnumSource(PersonTestEnum.class)
                public void findById(PersonTestEnum testPerson) {
                  assertNotNull(classUnderTest);
                  Person person = testPerson.getPerson();
                  Person personFound = classUnderTest.findById(person.getId());
                  assertNotNull(personFound);
                  performPersonAssertions(person.getLastName(), person.getFirstName(), person.getAge(), person.getEyeColor(),
                      person.getGender(), personFound);
                }

First, you tell JUnit that the findById() method is a @ParameterizedTest, as shown in line 1. Then you specify the enum's Java class, as shown in line 2. JUnit will invoke the findById() test method, each time passing the next enum value to the method (line 3), until the enum is exhausted. You use the parameter just as you would any Java method parameter (line 5).

Note that the PersonTestEnum class is part of the sample application that accompanies this tutorial. It is located in the src/test/java tree, in the com.makotojava.learn.junit package.

@MethodSource

With a @MethodSource, you can specify any complex object you like as the argument type for a test method. The syntax looks like this:

                  @ParameterizedTest
                  @MethodSource(value = "personProvider")
                  public void findById(Person paramPerson) {
                    assertNotNull(classUnderTest);
                    long id = paramPerson.getId();
                    Person personFound = classUnderTest.findById(id);
                    assertNotNull(personFound);
                    performPersonAssertions(paramPerson.getLastName(), paramPerson.getFirstName(),
                        paramPerson.getAge(),
                        paramPerson.getEyeColor(), paramPerson.getGender(), personFound);
                  }

The names attribute of the @MethodSource is used to specify one or more method names that provide the arguments for the test method. The return type of a method source must be Stream, Iterator, Iterable, or array. In addition, the provider method must be declared static, so you cannot use it inside of a @Nested test class (at least as of JUnit 5 Milestone 5).

In the above example, the personProvider method (from the sample application) looks like this:

                static Iterator<Person> personProvider() {
                    PersonTestEnum[] testPeople = PersonTestEnum.values();
                    Person[] people = new Person[testPeople.length];
                    for (int aa = 0; aa < testPeople.length; aa++) {
                      people[aa] = testPeople[aa].getPerson();
                    }
                    return Arrays.asList(people).iterator();
                }

Suppose you wanted an additional arguments provider for the test method. You would declare it like this:

                  @ParameterizedTest
                  @MethodSource(value = { "personProvider", "additionalPersonProvider" })
                  public void findById(Person paramPerson) {
                    assertNotNull(classUnderTest);
                    long id = paramPerson.getId();
                    Person personFound = classUnderTest.findById(id);
                    assertNotNull(personFound);
                    performPersonAssertions(paramPerson.getLastName(), paramPerson.getFirstName(),
                        paramPerson.getAge(),
                        paramPerson.getEyeColor(), paramPerson.getGender(), personFound);
                  }

The methods are specified using array initializer syntax (line 2), and will be called in the order you specify, with additionalPersonProvider() called last.

Customizing the display name

The default display name for parameterized tests contains the test index (a 1-based iteration number), along with a String representation of the argument. If you have multiple test methods in a test class, the output gets confusing. Fortunately, you can customize the output by supplying any of the following attribute values to the @ParameterizedTest annotation:

  • {index}: The 1-based index (the current test iteration).
  • {arguments}: The complete list of arguments, separated by commas.
  • {0}, {1} ...: A specific argument (0 is first, and so on).

Suppose, for example, that an array of five longs is supplied. In that case, annotating @ParameterizedTest like this

                @ParameterizedTest(name = "@ValueSource: FindById(): Test# {index}: Id: {0}")
                @ValueSource(longs = { 1L, 2L, 3L, 4L, 5L })
                public void findById(Long id) {
                  assertNotNull(classUnderTest);
                  Person personFound = classUnderTest.findById(id);
                  assertNotNull(personFound);
                  assertEquals(id, personFound.getId());
                }

would produce the following output:

                @ValueSource: FindById(): Test# 1: Id: 1
                @ValueSource: FindById(): Test# 2: Id: 2
                @ValueSource: FindById(): Test# 3: Id: 3
                @ValueSource: FindById(): Test# 4: Id: 4
                @ValueSource: FindById(): Test# 5: Id: 5

Dynamic tests

Up till now we have looked at static tests, which means the code for the test, the test data, and the test's pass/fail conditions are all known at compile time.

JUnit Jupiter introduces a new type of test called a dynamic test, which is generated at runtime by a special method called a test factory.

The @TestFactory

A @TestFactory method is used to generate dynamic tests. This method must return a Stream, Collection, Iterable, or Iterator of DynamicTest instances.

Unlike a @Test method, there are no lifecycle callbacks for a DynamicTest instance. So @BeforeEach, @AfterEach, and the other lifecycle callbacks from Table 1 do not apply to a DynamicTest.

Creating a @TestFactory

Consider the following code from the PersonDaoBeanTest class in the sample application (you can find it in the src/test/java tree of the com.makotojava.learn.junit5 package):

                @TestFactory
                @DisplayName("FindById - Dynamic Test Generator")
                Stream<DynamicTest> generateFindByIdDynamicTests() {
                  Long[] ids = { 1L, 2L, 3L, 4L, 5L };
                  return Stream.of(ids).map(id -> dynamicTest("DynamicTest: Find by ID " + id, () -> {
                    Person person = classUnderTest.findById(id);
                    assertNotNull(person);
                    int index = id.intValue() - 1;
                    Person testPerson = PersonTestEnum.values()[index].getPerson();
                    performPersonAssertions(testPerson.getLastName(), testPerson.getFirstName(),
                        testPerson.getAge(), testPerson.getEyeColor(), testPerson.getGender(), person);
                  }));
                }

The @TestFactory annotation marks this method as a factory for DynamicTests (line 1), and returns a Stream of DynamicTest instances as required by JUnit Jupiter (line 2). The tests generated by this @TestFactory don't do anything fancy; they just invoke findById on the PersonDaoBean Spring bean (line 6), and perform some assertions (lines 10 through 11). But this shows you how to create a dynamic test.

Tags and filtering

Tags are great for filtering tests. In this section I'll show you how to create a custom filter, then turn that into a composed annotation that can be used to control which tests are run.

Using @Tags

A JUnit Jupiter tag describes the use of the @Tag annotation, which creates a new identifier (the tag), and takes a single String parameter to uniquely identify the tag. Here are some examples:

                @Tag("foo")
                @Tag("bar")
                @Tag("advanced")

You use a tag to annotate a method or a class, like this:

                @Tag("advanced")
                @TestFactory
                @DisplayName("FindById - Dynamic Test Generator")
                Stream<DynamicTest> generateFindByIdDynamicTests() {
                  Long[] ids = { 1L, 2L, 3L, 4L, 5L, 6L };
                  return Stream.of(ids).map(id -> dynamicTest("DynamicTest: Find by ID " + id, () -> {
                    Person person = classUnderTest.findById(id);
                    assertNotNull(person);
                    int index = id.intValue() - 1;
                    Person testPerson = PersonTestEnum.values()[index].getPerson();
                    performPersonAssertions(testPerson.getLastName(), testPerson.getFirstName(),
                        testPerson.getAge(), testPerson.getEyeColor(), testPerson.getGender(), person);
                  }));
                }

You can then use filter settings in the Maven POM or Gradle build script to filter out this test. I'll show you how to do that later in the tutorial.

Creating your own composed annotation

@Tags get more interesting when you use them to create new composed annotations, rather than using the @Tag and its unique name. Remember the @Tag("advanced") from the previous section? I could create a new composed annotation to represent an advanced type of tests, like this:

Listing 6. Creating a composed annotation
                import static java.lang.annotation.ElementType.METHOD;
                import static java.lang.annotation.ElementType.TYPE;
                import static java.lang.annotation.RetentionPolicy.RUNTIME;
                
                import java.lang.annotation.Retention;
                import java.lang.annotation.Target;
                
                import org.junit.jupiter.api.Tag;
                
                @Retention(RUNTIME)
                @Target({ TYPE, METHOD })
                @Tag("advanced")
                public @interface Advanced {
                  // Nothing to do
                }

Now everywhere I would have used @Tag("advanced"), I use @Advanced instead, like this:

                @Advanced
                @TestFactory
                @DisplayName("FindById - Dynamic Test Generator")
                Stream<DynamicTest> generateFindByIdDynamicTests() {
                  Long[] ids = { 1L, 2L, 3L, 4L, 5L, 6L };
                  return Stream.of(ids).map(id -> dynamicTest("DynamicTest: Find by ID " + id, () -> {
                    Person person = classUnderTest.findById(id);
                    assertNotNull(person);
                    int index = id.intValue() - 1;
                    Person testPerson = PersonTestEnum.values()[index].getPerson();
                    performPersonAssertions(testPerson.getLastName(), testPerson.getFirstName(),
                        testPerson.getAge(), testPerson.getEyeColor(), testPerson.getGender(), person);
                  }));
                }

As I mentioned, you can use the new composed annotation at either the class level or the method level (thanks to the @Target annotation; see line 11 in Listing 6). Have a look at the PersonDaoBeanRepeatedTest class in the sample application to see this in action, where I have annotated the entire class with @Advanced. And in PersonDaoBeanTest, I have marked only the generateFindByIdDynamicTests() method that generates dynamic tests as @Advanced.

Running with Maven

In Part 1, I showed you how to run JUnit tests with Maven and Gradle. In this section, I'll show you how to configure your Maven POM to filter out the @Advanced tests from the sample application.

The JUnit User's Guide contains a much more detailed reference to the various Maven configuration settings, so I refer you there if you need additional information.

To try it out, first run the build, and notice the number of tests that are run (line 12 below). You should see something like this:

$ mvn clean test
.
.
-------------------------------------------------------
 T E S T S
-------------------------------------------------------
May 22, 2017 10:04:15 AM org.junit.jupiter.engine.discovery.JavaElementsResolver resolveClass
.
.
Results :

Tests run: 92, Failures: 0, Errors: 0, Skipped: 0

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 55.276 s
[INFO] Finished at: 2017-05-22T10:05:08-05:00
[INFO] Final Memory: 19M/297M
[INFO] ------------------------------------------------------------------------
$

Make note of how many tests are run (line 12) so you can compare that number with the value after you apply the filter.

Open the POM in Eclipse, and locate the Maven surefire plugin:

                <build>
                	<plugins>
                    .
                    .
                		<plugin>
                			<artifactId>maven-surefire-plugin</artifactId>
                			<version>2.19.1</version>
                    .
                    .
                </build>
                	</plugins>

Now modify the POM just below the version element (line 7) so that it looks like this:

                <build>
                	<plugins>
                    .
                    .
                		<plugin>
                			<artifactId>maven-surefire-plugin</artifactId>
                			<version>2.19.1</version>
                			<configuration>
                				<properties>
                					<excludeTags>advanced</excludeTags>
                				</properties>
                			</configuration>
                    .
                    .
                	</plugins>
                </build>

Run the build again, and you should see fewer tests run. The output should look something like this:

$mvn clean test
.
.
-------------------------------------------------------
 T E S T S
-------------------------------------------------------
May 22, 2017 10:09:42 AM org.junit.jupiter.engine.discovery.JavaElementsResolver resolveClass
.
.
Results :

Tests run: 47, Failures: 0, Errors: 0, Skipped: 0

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 32.023 s
[INFO] Finished at: 2017-05-22T10:10:11-05:00
[INFO] Final Memory: 20M/300M
[INFO] ------------------------------------------------------------------------
$

Notice that there are far fewer tests run with the filtering in place (line 12) than previously.

Running with Gradle

Next, I'll show you how to configure your Gradle build script to filter out the @Advanced tests from the sample application.

The JUnit User's Guide contains a much more detailed reference to the various Gradle configuration settings, so I refer you there if you need additional information.

To try it out, first run the build, and notice the number of tests that are run (lines 19, 21, and 23). You should see something like this:

                $ gradle clean test
                :clean
                :compileJava
                :processResources NO-SOURCE
                :classes
                :compileTestJava
                :processTestResources
                :testClasses
                :junitPlatformTest
                .
                .
                Test run finished after 62083 ms
                [        24 containers found      ]
                [         0 containers skipped    ]
                [        24 containers started    ]
                [         0 containers aborted    ]
                [        24 containers successful ]
                [         0 containers failed     ]
                [        92 tests found           ]
                [         0 tests skipped         ]
                [        92 tests started         ]
                [         0 tests aborted         ]
                [        92 tests successful      ]
                [         0 tests failed          ]
                
                :test SKIPPED
                
                BUILD SUCCESSFUL
                
                Total time: 1 mins 3.718 secs
                $

Make a note of how many tests are run (line 23) so you can compare that number with the value after you apply the filter.

Open the build script in Eclipse, and locate the junitplatform section, which looks like this:

                junitPlatform {
                  filters {
                    engines {
                    }
                    tags {
                    }
                  }
                  logManager 'org.apache.logging.log4j.jul.LogManager'
                }

Now modify the POM just below the tags element, so that it looks like this:

                junitPlatform {
                  filters {
                    engines {
                    }
                    tags {
                        exclude 'advanced'
                    }
                  }
                  logManager 'org.apache.logging.log4j.jul.LogManager'
                }

Run the build again, and you should see fewer tests run. The output should look something like this:

                $ gradle clean test
                :clean
                :compileJava
                :processResources NO-SOURCE
                :classes
                :compileTestJava
                :processTestResources
                :testClasses
                :junitPlatformTest
                .
                .
                Test run finished after 38834 ms
                [        13 containers found      ]
                [         0 containers skipped    ]
                [        13 containers started    ]
                [         0 containers aborted    ]
                [        13 containers successful ]
                [         0 containers failed     ]
                [        47 tests found           ]
                [         0 tests skipped         ]
                [        47 tests started         ]
                [         0 tests aborted         ]
                [        47 tests successful      ]
                [         0 tests failed          ]
                
                :test SKIPPED
                
                BUILD SUCCESSFUL
                
                Total time: 40.487 secs
                $

Notice how many fewer tests are run with the filtering in place (line 19,21,23) then before.

Conclusion

This second half of the JUnit 5 tutorial has focused on JUnit Vintage and the JUnit Jupiter Extension Model. JUnit Vintage provides backward compatibility to JUnit 3 and JUnit 4, and the JUnit Jupiter Extension Model enables you to extend the JUnit Jupiter API for third-party tools or custom testing scenarios. I summarized the new extension points available in JUnit Jupiter, then walked you through a series of examples highlighting new, extensible features for parameter injection, parameterized tests, dynamic tests, and custom annotations in JUnit 5.

Congratulations! You've completed the JUnit 5 tutorial. If you haven't already, I encourage you to watch the video that accompanies this tutorial. The video expands on what you've learned so far about the new ParameterResolver and @ParameterizedTest features, and also shows you how to run the console launcher that is included with the HelloJUnit5Part2 source code.


Downloadable resources


Related topics


Comments

Sign in or register to add and subscribe to comments.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Java development, Open source
ArticleID=1047643
ArticleTitle=Introducing JUnit 5, Part 2: JUnit 5 Vintage and the JUnit Jupiter Extension Model
publish-date=12112017