Testing with Randomized Inputs in JUnit

Illustration for Testing with Randomized Inputs in JUnit
By Last updated:

One of the biggest risks in software development is assuming that your code will always run with “expected” inputs. In reality, users and systems often provide unexpected or invalid data. To uncover hidden bugs, randomized testing (also called fuzz testing) introduces variability by generating random inputs during test execution.

JUnit 5 makes it easy to integrate randomized inputs with @RepeatedTest, ParameterizedTest, and custom providers. This approach helps developers catch edge cases that might slip through deterministic unit tests.


Why Randomized Testing Matters

  • Discover Edge Cases: Random inputs reveal unexpected failures.
  • Improve Coverage: Test beyond hardcoded values.
  • Catch Flaky Behavior: Identify intermittent failures in logic.
  • CI/CD Resilience: Increase confidence in production readiness.
  • Performance: Simulate diverse workloads for robustness.

Think of randomized testing like stress-testing a bridge — you don’t just test it with one car, but with vehicles of different sizes, weights, and speeds.


Basic Randomized Test with @RepeatedTest

You can use @RepeatedTest to run code multiple times with different random values.

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

import java.util.Random;

class RandomizedTestExample {

    private final Random random = new Random();

    @RepeatedTest(5)
    void shouldGenerateRandomNumbers() {
        int value = random.nextInt(100);
        System.out.println("Generated: " + value);
        assertTrue(value >= 0 && value < 100);
    }
}

This ensures five different executions with random numbers.


Parameterized Tests with Random Sources

JUnit 5’s @ParameterizedTest can be combined with custom sources.

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

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

class RandomParameterizedTest {

    static Stream<Integer> randomNumbers() {
        Random random = new Random();
        return IntStream.range(0, 5).mapToObj(i -> random.nextInt(100));
    }

    @ParameterizedTest
    @MethodSource("randomNumbers")
    void shouldRunWithRandomInputs(Integer number) {
        System.out.println("Testing with: " + number);
        assertTrue(number >= 0 && number < 100);
    }
}

Using Randomized Inputs for Edge Cases

Random values can validate exception handling.

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.util.Random;

class RandomEdgeCaseTest {

    @Test
    void shouldThrowForInvalidDivision() {
        Random random = new Random();
        int denominator = random.nextBoolean() ? 0 : random.nextInt(10);
        
        if (denominator == 0) {
            assertThrows(ArithmeticException.class, () -> {
                int result = 10 / denominator;
            });
        }
    }
}

Combining Randomization with Mockito

Mockito can simulate random failures in external services.

import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import static org.mockito.Mockito.*;

import java.util.Random;

class RandomMockitoTest {

    interface ExternalService {
        String fetchData();
    }

    @Test
    void shouldSimulateRandomFailures() {
        ExternalService service = mock(ExternalService.class);
        Random random = new Random();

        when(service.fetchData()).thenAnswer(invocation -> {
            if (random.nextBoolean()) {
                throw new RuntimeException("Random failure");
            }
            return "data";
        });

        try {
            String result = service.fetchData();
            System.out.println("Success: " + result);
        } catch (RuntimeException e) {
            System.out.println("Caught error: " + e.getMessage());
        }
    }
}

Randomized Testing with Testcontainers

You can combine randomization with Testcontainers for database testing.

import org.junit.jupiter.api.Test;
import org.testcontainers.containers.PostgreSQLContainer;

import java.util.Random;

class RandomTestcontainersTest {

    @Test
    void shouldInsertRandomData() {
        try (PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")) {
            postgres.start();
            System.out.println("JDBC URL: " + postgres.getJdbcUrl());

            Random random = new Random();
            int randomId = random.nextInt(1000);
            System.out.println("Inserting record with ID: " + randomId);
            assert randomId >= 0;
        }
    }
}

Sample output:

Creating container for image: postgres:15
Container started in 12.3s
Inserting record with ID: 582

Real-World Scenarios

  1. Microservices: Randomize API payloads to uncover serialization issues.
  2. Databases: Insert randomized test data to detect indexing problems.
  3. Legacy Apps: Stress-test old logic with unexpected inputs.
  4. CI/CD: Run randomized subsets of tests to increase confidence before release.

Best Practices

  • Use random tests in addition to deterministic tests.
  • Log random seeds for reproducibility (new Random(seed)).
  • Tag randomized tests (@Tag("random")) to control execution in pipelines.
  • Avoid excessive randomness in CI builds (use seeds for consistency).
  • Combine with @RepeatedTest for wider coverage.

Version Tracker

  • JUnit 4 → JUnit 5: JUnit 5 introduced richer parameterized tests and repeated tests.
  • Mockito Updates: Inline mocking improves error simulation in randomized tests.
  • Testcontainers Growth: New modules allow dynamic DB/container interactions with random inputs.

Conclusion & Key Takeaways

Randomized testing in JUnit helps you catch edge cases early, strengthen error handling, and improve overall application resilience. By integrating random values with JUnit 5, Mockito, and Testcontainers, you can uncover hidden issues that deterministic tests miss.

Key Takeaways:

  • Use @RepeatedTest and @ParameterizedTest for random inputs.
  • Validate error handling with unexpected values.
  • Combine randomization with mocks and Testcontainers.
  • Log random seeds for reproducibility.

FAQ

1. What is the benefit of randomized tests?
They uncover hidden bugs by exploring unexpected input scenarios.

2. Can randomized tests replace deterministic tests?
No, they should complement deterministic tests, not replace them.

3. How do I make random tests reproducible?
Log the random seed and rerun with the same seed.

4. Do random tests work with CI/CD?
Yes, but use seeds or limit scope for predictability.

5. How do I avoid flaky random tests?
Control randomness with seeds and validate only essential conditions.

6. Can I use randomized tests with parameterized tests?
Yes, you can supply random data via @MethodSource or @ValueSource.

7. Do random tests slow down pipelines?
They can, so use tags (@Tag("random")) to separate them.

8. Can Mockito simulate random failures?
Yes, with thenAnswer to throw exceptions conditionally.

9. Do Testcontainers support randomization?
Yes, you can insert random data or run tests with varied configurations.

10. Should I migrate to JUnit 5 for random tests?
Yes, JUnit 5 offers richer support with @RepeatedTest, parameterized tests, and extensions.