Dynamic tests in JUnit 5 allow you to create tests at runtime, providing a flexible and powerful way to generate test cases dynamically. This feature is particularly useful when the number of test cases is not known beforehand or when you want to generate tests based on some runtime conditions.

Key Concepts

  1. Dynamic Tests: Tests that are generated at runtime.
  2. @TestFactory: Annotation used to indicate a method that generates dynamic tests.
  3. DynamicTest: A class representing a single dynamic test case.
  4. Stream, Collection, Iterable: Dynamic tests can be returned as a Stream, Collection, or Iterable.

Creating Dynamic Tests

Using @TestFactory

The @TestFactory annotation is used to mark a method that generates dynamic tests. This method should return a Stream, Collection, or Iterable of DynamicTest instances.

Example

Here is a simple example of creating dynamic tests using @TestFactory:

import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;
import org.junit.jupiter.api.function.Executable;

import java.util.Arrays;
import java.util.Collection;

import static org.junit.jupiter.api.Assertions.assertTrue;

public class DynamicTestsExample {

    @TestFactory
    Collection<DynamicTest> dynamicTests() {
        return Arrays.asList(
            DynamicTest.dynamicTest("Test 1", () -> assertTrue(isEven(2))),
            DynamicTest.dynamicTest("Test 2", () -> assertTrue(isEven(4))),
            DynamicTest.dynamicTest("Test 3", () -> assertTrue(isEven(6)))
        );
    }

    boolean isEven(int number) {
        return number % 2 == 0;
    }
}

Explanation

  • @TestFactory: Marks the method dynamicTests as a factory for dynamic tests.
  • DynamicTest.dynamicTest: Creates a new dynamic test with a display name and an executable (the test logic).
  • assertTrue: Asserts that the condition is true.

Using Streams

Dynamic tests can also be generated using streams, which is useful for large or infinite sequences of tests.

import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;

import java.util.stream.IntStream;
import java.util.stream.Stream;

import static org.junit.jupiter.api.Assertions.assertTrue;

public class DynamicTestsStreamExample {

    @TestFactory
    Stream<DynamicTest> dynamicTestsFromStream() {
        return IntStream.range(0, 10)
            .mapToObj(n -> DynamicTest.dynamicTest("Test " + n, () -> assertTrue(isEven(n * 2))));
    }

    boolean isEven(int number) {
        return number % 2 == 0;
    }
}

Explanation

  • IntStream.range(0, 10): Generates a stream of integers from 0 to 9.
  • mapToObj: Maps each integer to a DynamicTest.
  • DynamicTest.dynamicTest: Creates a new dynamic test for each integer.

Practical Exercise

Exercise

Create a dynamic test that verifies if a list of strings contains only non-empty strings.

  1. Create a method isNonEmpty that checks if a string is non-empty.
  2. Use @TestFactory to generate dynamic tests for a list of strings.
  3. Use assertTrue to assert that each string is non-empty.

Solution

import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;

import java.util.Arrays;
import java.util.Collection;

import static org.junit.jupiter.api.Assertions.assertTrue;

public class NonEmptyStringTests {

    @TestFactory
    Collection<DynamicTest> dynamicTestsForNonEmptyStrings() {
        return Arrays.asList(
            DynamicTest.dynamicTest("Test 1", () -> assertTrue(isNonEmpty("Hello"))),
            DynamicTest.dynamicTest("Test 2", () -> assertTrue(isNonEmpty("JUnit"))),
            DynamicTest.dynamicTest("Test 3", () -> assertTrue(isNonEmpty("Dynamic Tests")))
        );
    }

    boolean isNonEmpty(String str) {
        return str != null && !str.isEmpty();
    }
}

Explanation

  • isNonEmpty: Checks if a string is non-empty.
  • dynamicTestsForNonEmptyStrings: Generates dynamic tests for a list of strings.
  • assertTrue: Asserts that each string is non-empty.

Common Mistakes and Tips

  • Returning Null: Ensure that the @TestFactory method does not return null. It should return a non-null Stream, Collection, or Iterable.
  • Descriptive Names: Use descriptive names for dynamic tests to make it easier to identify which test failed.
  • Avoiding Side Effects: Ensure that the test logic in dynamic tests does not have side effects that could affect other tests.

Conclusion

Dynamic tests in JUnit 5 provide a powerful way to generate tests at runtime, offering flexibility and scalability. By using @TestFactory and DynamicTest, you can create dynamic test cases based on runtime conditions, making your test suite more robust and adaptable.

© Copyright 2024. All rights reserved