Skip to content

ConcurrentModificationException in TestExecutionCondition when running tests with JUnit parallel class execution #1789

Description

@arendomoda

Problem

With spring-modulith-junit on the classpath and JUnit 5 parallel class execution
enabled, test classes fail before executing with:

org.junit.jupiter.engine.execution.ConditionEvaluationException:
    Failed to evaluate condition [org.springframework.modulith.junit.ModulithExecutionCondition]
Caused by: java.util.ConcurrentModificationException
    at org.springframework.modulith.junit.TestExecutionCondition.lambda$evaluate$1(TestExecutionCondition.java:90)
    at org.springframework.modulith.junit.TestExecutionCondition.evaluate(TestExecutionCondition.java:84)
    at org.springframework.modulith.junit.ModulithExecutionCondition.lambda$evaluateExecutionCondition$0(ModulithExecutionCondition.java:70)
    at org.springframework.modulith.junit.ModulithExecutionCondition.evaluateExecutionCondition(ModulithExecutionCondition.java:63)

74+ of our 602 test classes failed this way in a single run, and it hits a different
set of classes on every run. Each failure reports Time elapsed: 0.001 s, i.e. the
condition evaluation itself fails, not the test.

Note this only surfaces when a reference commit is configured and the extension
actually computes module dependencies. With the default detector (no changes
detected, full suite executed) the code path is not reached, so the problem stays
hidden until change-aware selection is genuinely active.

Suspected cause

TestExecutionCondition caches module dependencies in an unsynchronized map:

private final Map<ApplicationModule, ApplicationModuleDependencies> dependencies = new HashMap<>();
...
var dependencies = this.dependencies.computeIfAbsent(it, m -> m.getAllDependencies(modules));

With junit.jupiter.execution.parallel.mode.classes.default=concurrent, multiple
threads evaluate the condition concurrently against the same instance. This looks
like the same class of problem as gh-1295 (MODULITH_TYPES, fixed for 2.0 M1),
but on a different field, so the earlier fix does not cover it.

The field is still a plain HashMap on main and in 2.1.0.

Suggested fix

Use ConcurrentHashMap for the dependencies field (no public API change).

Environment

  • spring-modulith 2.0.6 (also reproducible per source inspection on 2.1.0 / main)
  • Spring Boot 4.0.6, Java 25, Maven Surefire with forkCount=1, reuseForks=true
  • 3396 tests in 602 classes, 13 application modules
  • junit-platform.properties:
junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.mode.default=same_thread
junit.jupiter.execution.parallel.mode.classes.default=concurrent
junit.jupiter.execution.parallel.config.strategy=fixed
junit.jupiter.execution.parallel.config.fixed.parallelism=6
  • spring.modulith.test.reference-commit set to a git SHA via environment variable

Workaround

Removing the spring-modulith-junit dependency (or setting
spring.modulith.test.skip-optimizations=true) avoids it, at the cost of losing
change-aware test selection.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions