To continue the series about JUnit 5 tutorial, I’d like to introduce about JUnit 5 Dynamic Tests feature which allows us to declare and run test cases generated at run-time.
1. Static Tests vs Dynamic Tests
1.1. Static Tests
To get to know about the Dynamic Tests vs Static Tests, let take a look at an example below. We have a very simple TranslatorEngine class which is responsible for translating text from English to French. Note that the class is implemented basically, without optimization to make it easy to understand.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
public class TranslatorEngine { public String tranlate(String text) { if (StringUtils.isBlank(text)) { throw new IllegalArgumentException("Translated text must not be empty."); } if ("Hello".equalsIgnoreCase(text)) { return "Bonjour"; } else if ("Yes".equalsIgnoreCase(text)) { return "Oui"; } else if ("No".equalsIgnoreCase(text)) { return "Non"; } else if ("Goodbye".equalsIgnoreCase(text)) { return "Au revoir"; } else if ("Good night".equalsIgnoreCase(text)) { return "Bonne nuit"; } else if ("Thank you".equalsIgnoreCase(text)) { return "Merci"; } else { return "Not found"; } } } |
Now, let’s write some tests for this class. Basically, we can come up with several test cases as follows. Note that these test cases are still not enough for the translate method yet.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
import static org.junit.jupiter.api.Assertions.assertEquals; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.platform.runner.JUnitPlatform; import org.junit.runner.RunWith; @RunWith(JUnitPlatform.class) public class TranslationEngineTest { private TranslatorEngine translatorEngine; @BeforeEach public void setUp() { translatorEngine = new TranslatorEngine(); } @Test public void testTranlsateHello() { assertEquals("Bonjour", translatorEngine.tranlate("Hello")); } @Test public void testTranlsateYes() { assertEquals("Oui", translatorEngine.tranlate("Yes")); } @Test public void testTranlsateNo() { assertEquals("Non", translatorEngine.tranlate("No")); } } |
These above tests are called Static Tests. They are are static in the sense that they are fully specified at compile-time, and their behavior cannot be changed at run-time. Note that we have just written 3 test cases while the current translate method can support translate 6 words/phrases. We may need to add 3 more static tests for 3 remain words/phrases and other cases like Null or empty word/phrases, not supported words/phrases, etc.
When we the test runner runs, basically, it will look for all the tests which defined by annotating the @Test annotation and run them.
If the translate method supports for more word/phrases/sentences, say 1000, we may need to add 1000 more test cases tediously for this method.
1.2. Dynamic Tests
In contrary to the Static Tests which allow us to define statically a number of fixed test cases at the compile time, the Dynamic Tests allow us to define the tests case dynamically in the run-time.
Let’s see an example about Dynamic Tests as following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
public void translateDynamicTests() { List<String> inPhrases = new ArrayList<>(Arrays.asList("Hello", "Yes", "No")); List<String> outPhrases = new ArrayList<>(Arrays.asList("Bonjour", "Oui", "Non")); Collection<DynamicTest> dynamicTests = new ArrayList<>(); for (int i = 0; i < inPhrases.size(); i++) { String phr = inPhrases.get(i); String outPhr = outPhrases.get(i); // create an test execution Executable exec = () -> assertEquals(outPhr, translatorEngine.tranlate(phr)); // create a test display name String testName = " Test tranlate " + phr; // create dynamic test DynamicTest dTest = DynamicTest.dynamicTest(testName, exec); // add the dynamic test to collection dynamicTests.add(dTest); } } |
We have tried to create a collection of test cases by iterating through the list of data. Those tests are dynamic in the sense that they are generated in the run-time, and the number of test cases is depended on the data, number of the input words/phrases.
That was a simple demo example about dynamic tests. Let’s see in detail how we can fully create dynamic tests by using JUnit 5 Dynamic Tests feature in the following section.
2. JUnit 5 Dynamic Tests
In JUnit 5, dynamic test cases are represented by DynamicTest class. Besides, dynamic tests have some more essential points as following:
- Dynamic tests can be generated by a factory method annotated with @TestFactory which is a new annotation of JUnit 5
- @TestFactory method must return a Stream, Collection, Iterable, or Iterator of DynamicTest instances
- @TestFactory methods must not be private or static and may optionally declare parameters to be resolved by ParameterResolvers
2.1. Dynamic Tests Examples
In this part, there will be some examples about JUnit 5 Dynamic Tests feature. All the example source code can be found on Github. To run the examples, you will need to get JUnit 5 be ready for your environment and you can refer to the following tutorials for quickly getting things done:
Next, we will modify the above test method to make it comply with the syntax of JUnit 5 Dynamic Tests.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
@TestFactory public Collection<DynamicTest> translateDynamicTests() { List<String> inPhrases = new ArrayList<>(Arrays.asList("Hello", "Yes", "No", "Goodbye", "Good night", "Thank you")); List<String> outPhrases = new ArrayList<>(Arrays.asList("Bonjour", "Oui", "Non", "Au revoir", "Bonne nuit", "Merci")); Collection<DynamicTest> dynamicTests = new ArrayList<>(); for (int i = 0; i < inPhrases.size(); i++) { String phr = inPhrases.get(i); String outPhr = outPhrases.get(i); // create an test execution Executable exec = () -> assertEquals(outPhr, translatorEngine.tranlate(phr)); // create a test display name String testName = "Test tranlate " + phr; // create dynamic test DynamicTest dTest = DynamicTest.dynamicTest(testName, exec); // add the dynamic test to collection dynamicTests.add(dTest); } return dynamicTests; } |
We have annotated the method with the @TestFactory annotation and changed the return type to Collection<DynamicTest>.
This example is very straight forward to easy to understand. Let’s tune it to be complied with Java 8 style and return a Stream of DynamicTest instead of a collection.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@TestFactory public Stream<DynamicTest> translateDynamicTestsFromStream() { List<String> inPhrases = new ArrayList<>(Arrays.asList("Hello", "Yes", "No", "Goodbye", "Good night", "Thank you")); List<String> outPhrases = new ArrayList<>(Arrays.asList("Bonjour", "Oui", "Non", "Au revoir", "Bonne nuit", "Merci")); return inPhrases.stream().map(phrs -> DynamicTest.dynamicTest("Test translate " + phrs, () -> { int idx = inPhrases.indexOf(phrs); assertEquals(outPhrases.get(idx), translatorEngine.tranlate(phrs)); })); } |
As mentioned above, the factory method must return a Stream, Collection, Iterable, or Iterator. We will try to return an Iterable of DynamicTest instances.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@TestFactory public Iterable<DynamicTest> translateDynamicTestsFromIterate() { List<String> inPhrases = new ArrayList<>(Arrays.asList("Hello", "Yes", "No", "Goodbye", "Good night", "Thank you")); List<String> outPhrases = new ArrayList<>(Arrays.asList("Bonjour", "Oui", "Non", "Au revoir", "Bonne nuit", "Merci")); return inPhrases.stream().map(phrs -> DynamicTest.dynamicTest("Test translate " + phrs, () -> { int idx = inPhrases.indexOf(phrs); assertEquals(outPhrases.get(idx), translatorEngine.tranlate(phrs)); })).collect(Collectors.toList()); } |
And finally, we will try to return an Iterator of DynamicTest instances.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@TestFactory public Iterator<DynamicTest> translateDynamicTestsFromIterator() { List<String> inPhrases = new ArrayList<>(Arrays.asList("Hello", "Yes", "No", "Goodbye", "Good night", "Thank you")); List<String> outPhrases = new ArrayList<>(Arrays.asList("Bonjour", "Oui", "Non", "Au revoir", "Bonne nuit", "Merci")); return inPhrases.stream().map(phrs -> DynamicTest.dynamicTest("Test translate " + phrs, () -> { int idx = inPhrases.indexOf(phrs); assertEquals(outPhrases.get(idx), translatorEngine.tranlate(phrs)); })).iterator(); } |
2.2. Running Tests
To run the test on Eclipse, simply Right Click –> Run As –> JUnit Tests.
As for running with Maven and Gradle, you can refer to this post: JUnit 5 Basic Introduction
Here is the output on my Eclipse:
3. Summary.
We have gotten to know about JUnit 5 Dynamic Test feature which allows us to create test cases at the run-time. In my opinion, the Dynamic Test is necessary and help reduce effort in writing tests. However, Dynamic Tests is still an experimental feature in the current version of JUnit 5, 5.0.0-M2. This feature may be removed without prior notice. Besides, the execution lifecycle of a dynamic test is quite different with a standard @Test case. One essential point should be noticed that there are not any lifecycle callbacks for dynamic tests. This means that @BeforeEach and @AfterEach methods and their corresponding extension callbacks are not executed for dynamic tests.
In future posts, I’d like to continue deep dive into other features of JUnit 5. Recently, I have some posts related to JUnit 5 tutorial. If you’re interested in them, you can find them in the following links:
JUnit 5 Disable or Ignore A Test
JUnit 5 Test Suite – Aggregating Tests In Suites
JUnit 5 Assumptions With Assume
JUnit 5 Parameter Resolution Example
Display Names and Technical Names in JUnit 5