JUnit 6 Assumptions and Conditional Test Execution Guide

Sometimes a test should not run at all — not because it is broken, but because the environment or context makes it irrelevant. JUnit 6 Assumptions let you express these conditions explicitly: if an assumption fails, the test is skipped (aborted), not failed. This is a crucial distinction that keeps your test reports clean and meaningful.

This guide covers all assumption methods, conditional test annotations, and real-world scenarios where each approach shines.

Assumptions vs Assertions: The Key Difference

AssertionAssumption
ClassAssertionsAssumptions
On failureTest FAILSTest is SKIPPED/ABORTED
Use whenVerifying correct behaviourPrecondition for test relevance
Report statusRed ❌ FAILEDYellow ⚠️ ABORTED / SKIPPED

Think of assumptions as: “This test only makes sense if this condition is true. If it isn’t, skip it — it’s not a code bug, it’s just irrelevant right now.”

The Assumptions Class

Import all assumption methods with a static import:

import static org.junit.jupiter.api.Assumptions.*;

assumeTrue

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assumptions.*;
import static org.junit.jupiter.api.Assertions.*;

class DatabaseIntegrationTest {

    @Test
    @DisplayName("Fetch user from live database")
    void fetchUserFromDatabase() {
        // assumeTrue: if the condition is FALSE, this test is SKIPPED (not failed)
        // Use when the test is only relevant in certain environments
        assumeTrue(
            System.getenv("CI_ENV") != null,
            "Skipping: this test only runs in the CI environment"
        );

        // If we reach here, CI_ENV is set — proceed with the real test
        User user = userRepository.findById(1L);
        assertNotNull(user, "User with ID 1 should exist in the CI database");
    }

    @Test
    @DisplayName("Windows-only file locking test")
    void windowsFileLockingBehaviour() {
        // Only run this test on Windows
        assumeTrue(
            System.getProperty("os.name").toLowerCase().contains("win"),
            "Skipping: Windows-specific file locking test"
        );

        // Windows-specific assertion
        assertTrue(fileLockingService.isFileLocked("test.dat"));
    }
}

assumeFalse

@Test
@DisplayName("Skip test if running under memory profiler")
void performanceTest() {
    // assumeFalse: test is SKIPPED if the condition is TRUE
    boolean isRunningUnderProfiler = ManagementFactory
        .getRuntimeMXBean().getInputArguments().toString()
        .contains("javaagent");

    assumeFalse(isRunningUnderProfiler,
        "Skipping performance test: a profiler agent is attached");

    // If we get here, no profiler — timing is reliable
    long startTime = System.nanoTime();
    dataProcessor.processLargeDataset();
    long durationMs = (System.nanoTime() - startTime) / 1_000_000;

    assertTrue(durationMs < 1000,
        "Processing should complete within 1000 ms, took: " + durationMs + " ms");
}

assumingThat — Conditional Partial Test Execution

assumingThat is unique: if its condition is false, only the block inside it is skipped — the rest of the test continues. This is useful when part of a test is environment-specific but the core behaviour should always be verified:

@Test
@DisplayName("Order creation with environment-specific additional checks")
void orderCreationWithConditionalChecks() {
    Order order = orderService.createOrder("[email protected]", 99.99);

    // These assertions ALWAYS run regardless of environment
    assertNotNull(order);
    assertEquals(OrderStatus.PENDING, order.getStatus());

    // This block ONLY runs in production environment
    // If condition is false, this block is skipped but the test continues
    assumingThat(
        "production".equals(System.getenv("APP_ENV")),
        () -> {
            // Extra production-only checks
            assertNotNull(order.getConfirmationEmailSentAt(),
                "Confirmation email must be sent immediately in production");
            assertNotNull(order.getPaymentAuthorizationCode(),
                "Payment must be pre-authorised in production");
        }
    );

    // This assertion ALWAYS runs (after the conditional block)
    assertTrue(order.getTotalAmount() > 0,
        "Order total must always be positive");
}

Conditional Test Execution with Annotations

JUnit 6 also provides a set of @Enabled* and @Disabled* annotations for declarative conditional execution. These are evaluated before the test method is even called:

@EnabledOnOs and @DisabledOnOs

import org.junit.jupiter.api.condition.*;

class FileSystemTest {

    // Only runs on Linux and macOS
    @Test
    @EnabledOnOs({OS.LINUX, OS.MAC})
    @DisplayName("Unix file permissions test")
    void unixFilePermissionsAreSetCorrectly() {
        assertTrue(fileService.hasExecutePermission("/usr/local/bin/myapp"));
    }

    // Never runs on Windows
    @Test
    @DisabledOnOs(OS.WINDOWS)
    @DisplayName("Symbolic link creation test")
    void creatingSymbolicLinkSucceeds() throws IOException {
        Path symlink = Files.createSymbolicLink(
            Path.of("/tmp/test-link"), Path.of("/tmp/target")
        );
        assertTrue(Files.isSymbolicLink(symlink));
    }
}

@EnabledOnJre and @DisabledOnJre

class JavaVersionSpecificTest {

    // Only runs on Java 17 or higher
    @Test
    @EnabledOnJre({JRE.JAVA_17, JRE.JAVA_21})
    @DisplayName("Sealed class pattern matching test")
    void sealedClassPatternMatching() {
        // Java 17+ feature test
        Shape shape = new Circle(5.0);
        String result = switch (shape) {
            case Circle c  -> "Circle with radius " + c.radius();
            case Square s  -> "Square with side " + s.side();
        };
        assertEquals("Circle with radius 5.0", result);
    }

    // Disabled on Java 21+ (feature removed or changed)
    @Test
    @DisabledOnJre(JRE.JAVA_21)
    void legacyDateApiTest() {
        // Legacy API test skipped on Java 21
    }
}

@EnabledIfEnvironmentVariable and @EnabledIfSystemProperty

class EnvironmentAwareTest {

    // Only runs when the CI environment variable equals "true"
    @Test
    @EnabledIfEnvironmentVariable(named = "CI", matches = "true")
    @DisplayName("Full integration suite: CI only")
    void fullIntegrationSuiteRunsInCI() {
        // expensive integration test
    }

    // Only runs when the system property 'feature.flag.new-checkout' is 'enabled'
    @Test
    @EnabledIfSystemProperty(named = "feature.flag.new-checkout", matches = "enabled")
    @DisplayName("New checkout flow test")
    void newCheckoutFlowBehavesCorrectly() {
        // feature-flag-gated test
    }
}

Console Output: Skipped vs Failed

# When an assumption fails — test is ABORTED (not failed)
[INFO] Running DatabaseIntegrationTest
[WARNING] Tests run: 1, Failures: 0, Errors: 0, Skipped: 1

Aborted: Skipping: this test only runs in the CI environment
  at DatabaseIntegrationTest.fetchUserFromDatabase(DatabaseIntegrationTest.java:15)

# In IntelliJ IDEA: shown with a yellow/orange icon (not red)
# Build still PASSES — skipped tests do not fail the build

Frequently Asked Questions (FAQs)

Q1: Does a skipped test fail the build?

No. When an assumption fails, the test status is ABORTED (or SKIPPED), not FAILED. Maven Surefire and Gradle both count aborted tests separately and do not fail the build because of them. This is the fundamental reason to use assumptions instead of assertions for precondition checks.

Q2: What is the difference between assumeTrue and @EnabledIfEnvironmentVariable?

assumeTrue is evaluated inside the test method at runtime and can use any Java expression. @EnabledIfEnvironmentVariable is a declarative annotation evaluated before the test method is invoked. Use annotations for simple, static conditions (OS, JRE version, env variable value). Use assumeTrue for dynamic conditions that require code to evaluate.

Q3: Can I use assumeTrue inside @BeforeEach?

Yes. If assumeTrue fails in a @BeforeEach method, the corresponding test method is skipped. This is useful when an entire class of tests shares the same environmental precondition — put the assumption in @BeforeEach instead of repeating it in every test method.

Q4: How is assumingThat different from an if statement in a test?

Functionally similar, but assumingThat is semantically richer — it communicates clearly that you are conditionally executing test logic based on an environment assumption, not branching production-like logic. It also integrates with JUnit’s reporting infrastructure. Using if inside a test hides the conditional from the test framework; assumingThat is explicit and readable.

Q5: Can I combine multiple @Enabled conditions on one test?

Yes. Multiple @Enabled* annotations on the same method are combined with AND logic — all conditions must be true for the test to run. For example, @EnabledOnOs(OS.LINUX) combined with @EnabledIfEnvironmentVariable(named="CI", matches="true") means the test only runs on Linux in a CI environment.

See Also

Conclusion

Assumptions and conditional annotations are the professional way to handle environment-dependent tests. They keep your test suite honest — skipped tests signal “not applicable here” while failed tests signal “something is broken.” Use assumeTrue/assumeFalse for dynamic runtime conditions, assumingThat for partial conditional execution, and @Enabled*/@Disabled* annotations for declarative, static conditions.

Next: JUnit 6 Test Naming Conventions — the standards and patterns that make your test suite self-documenting and searchable.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.