Skip to main content

Scoring System

Each test class receives a priority score. Tests run in descending score order; among ties, faster tests go first.

Scoring Formula

score(t)=[t is new]wnew+15+[t source changed]wchanged+9+min ⁣(F(t),wfail)failure recency, 0..5+overlap(t)dep overlap, 0..5+complexity(t)change complexity, 0..2+[t same pkg]wpkg+2+speed(t)±1\text{score}(t) = \underbrace{[t\text{ is new}]\cdot w_{\text{new}}}_{\text{+15}} + \underbrace{[t\text{ source changed}]\cdot w_{\text{changed}}}_{\text{+9}} + \underbrace{\min\!\bigl(\lceil F(t)\rceil,\, w_{\text{fail}}\bigr)}_{\text{failure recency, 0..5}} + \underbrace{\text{overlap}(t)}_{\text{dep overlap, 0..5}} + \underbrace{\text{complexity}(t)}_{\text{change complexity, 0..2}} + \underbrace{[t\text{ same pkg}]\cdot w_{\text{pkg}}}_{\text{+2}} + \underbrace{\text{speed}(t)}_{\pm 1}

Sub-expressions:

F(t)=iei(1d)rid=0.3,ri=runs since failure iF(t) = \sum_{i} e_i \cdot (1 - d)^{\,r_i} \qquad d = 0.3,\quad r_i = \text{runs since failure } i overlap(t)=min ⁣(deps(t)Δmax(deps(t),5)woverlap,  woverlap)Δ=changed classes\text{overlap}(t) = \min\!\left(\left\lceil \frac{|\,\text{deps}(t)\cap\Delta\,|}{\sqrt{\max(|\text{deps}(t)|,\,5)}} \cdot w_{\text{overlap}} \right\rceil,\; w_{\text{overlap}}\right) \qquad \Delta = \text{changed classes} speed(t)=clamp ⁣(log2(dt/d~)3,1,1)dt=test duration,d~=suite median\text{speed}(t) = \operatorname{clamp}\!\left(\frac{\log_2(d_t / \tilde{d})}{3},\,-1,\,1\right) \qquad d_t = \text{test duration},\quad \tilde{d} = \text{suite median}

Full speed bonus at dtd~/8d_t \le \tilde{d}/8; full penalty at dt8d~d_t \ge 8\tilde{d}; zero at the median.

All weights are configurable. Override any signal via system properties (-Dtestorder.score.*=…) or a TOML weights file.

Score Components

SignalDefaultConfig keyNotes
New test15testorder.score.newTestAny test not yet in the dependency index
Changed test9testorder.score.changedTestTest source file itself was modified
Failure recency5 maxtestorder.score.maxFailureExponential decay — see below
Dep overlap5 maxtestorder.score.depOverlap\sqrt{}-normalised count of changed classes the test exercises
Change complexity2 maxtestorder.score.changeComplexityDeflate-entropy of changed files; disabled when coverageBonus > 0
Package proximity2testorder.score.packageProximityBonusTest package matches a changed class's package
Speed bonus1testorder.score.speedLogarithmic; full at dtd~/8d_t \le \tilde{d}/8
Speed penalty−1testorder.score.speedPenaltyLogarithmic; full at dt8d~d_t \ge 8\tilde{d}
Static field0testorder.score.staticFieldBonusOpt-in; requires MEMBER instrumentation mode
Coverage bonus0testorder.score.coverageBonusOpt-in; replaces dep overlap with greedy set-cover (×0.8rank\times 0.8^{\,\text{rank}})
Kill-rate0testorder.score.killRateBonusOpt-in; scaled by PIT mutation kill rate

Set any bonus to 0 to disable that signal entirely.

Change Complexity

Uses Deflate compression (JDK built-in) as an entropy proxy: larger compressed sizes indicate denser, more bug-prone changes. Scores are normalised to [0,1][0,1] relative to the largest changed file, summed over overlapping dependencies, then scaled by wchangeComplexityw_{\text{changeComplexity}}.

Disabled automatically when coverageBonus > 0.

Tie-breaking (Jaccard Diversity)

Equal-scored tests are ordered by greedy Jaccard-distance selection: each next test maximises the Jaccard distance between its dependency set and the union of dependencies already covered. This ensures breadth-first codebase coverage before redundant tests.

Jdist(A,B)=1ABABJ_{\text{dist}}(A,B) = 1 - \frac{|A \cap B|}{|A \cup B|}

Within a Jaccard tie: shorter historical duration wins, then alphabetical name.

Failure Scoring (Exponential Decay)

Each time the state file is saved after a completed test run:

scorefail(n+1)(t)=scorefail(n)(t)(1d)+[failed in run n]d=0.3\text{score}_{\text{fail}}^{(n+1)}(t) = \text{score}_{\text{fail}}^{(n)}(t) \cdot (1 - d) + [\text{failed in run } n] \qquad d = 0.3

Entries below failurePruneThreshold (default 0.01) are dropped. The final bonus is min(scorefail,wfail)\min(\lceil \text{score}_{\text{fail}} \rceil,\, w_{\text{fail}}).

ParameterConfig keyDefault
Decay per runfailureDecay0.3
Method-level decaymethodFailureDecay0.3
Prune thresholdfailurePruneThreshold0.01

Note: Decay only applies when a test run completes. Calling save() without a preceding run (e.g. from the weight optimizer) preserves scores unchanged.

Duration Smoothing (EMA)

Durations are smoothed with an exponential moving average to dampen outliers:

dt(n+1)=αdtmeasured+(1α)dt(n)α=0.85d_t^{(n+1)} = \alpha \cdot d_t^{\text{measured}} + (1-\alpha) \cdot d_t^{(n)} \qquad \alpha = 0.85

Both class-level (durationAlpha) and method-level (methodDurationAlpha) default to 0.85. Higher α\alpha tracks recent measurements more aggressively.

Customizing Scores

Default weights are defined in default-scoring-weights.toml. You can override them in three ways (highest priority first):

1. System properties — override individual weights:

mvn test -Dtestorder.score.newTest=20 -Dtestorder.score.changedTest=12

2. Weights file — provide a file with all customized weights:

mvn test -Dtestorder.weights.file=my-weights.toml

The file uses TOML format:

# my-weights.toml
[config]
failureDecay = 0.3
durationAlpha = 0.85

[newTest]
value = 20
range = [0, 50]

[changedTest]
value = 12
range = [0, 50]

[maxFailure]
value = 3
range = [1, 10]

[speed]
value = 2
range = [0, 10]

[speedPenalty]
value = 2
range = [0, 10]

[depOverlap]
value = 5
range = [0, 10]

[changeComplexity]
value = 2
range = [0, 5]

[staticFieldBonus]
value = 0
range = [0, 3]

Simple flat TOML format is also supported:

newTest = 20
changedTest = 12
maxFailure = 3
speed = 2
speedPenalty = 2
depOverlap = 5
changeComplexity = 2
staticFieldBonus = 0
coverageBonus = 0

3. Maven plugin configuration — set defaults in pom.xml:

<plugin>
<groupId>me.bechberger</groupId>
<artifactId>test-order-maven-plugin</artifactId>
<configuration>
<scoreNewTest>20</scoreNewTest>
<scoreChangedTest>12</scoreChangedTest>
<scoreMaxFailure>3</scoreMaxFailure>
<scoreSpeed>2</scoreSpeed>
<scoreSpeedPenalty>2</scoreSpeedPenalty>
<scoreDepOverlap>5</scoreDepOverlap>
<scoreChangeComplexity>2</scoreChangeComplexity>
<scoreStaticFieldBonus>0</scoreStaticFieldBonus>
<scoreCoverageBonus>0</scoreCoverageBonus>
</configuration>
</plugin>

Set a bonus to 0 to disable that scoring component entirely.

When to tune — and where to start

For most projects the defaults work well. The components worth tuning, in order of impact:

ComponentDefaultRaise when…Lower when…
newTest10New tests are frequently missed (team adds many tests per sprint)New tests consistently pass; you want to de-prioritise them
depOverlap5Change-detection is precise and you trust the indexMany tests share the same broad dependency set (large integration tests)
changeComplexity2Complex multi-file refactors often introduce regressionsAlmost all changes are single-class; complexity signal is noisy
speedPenalty2Slow tests are rarely responsible for failuresYou have a few very slow tests that do catch real regressions
maxFailure3Recent failures are a strong predictor (high-churn codebase)Failures are rare and clustered in old code you're cleaning up

Interpreting APFD: An APFD of 1.0 means every failing test ran before every passing test. For most projects, 0.7–0.85 with defaults is typical; above 0.85 suggests the defaults are already well-matched to your failure patterns. If APFD is below 0.6 consistently, run mvn test-order:optimize (see below) rather than tuning manually.

Automatic Score Tuning

Every order-mode test run records a quality snapshot to .test-order/state.lz4: per-test score breakdowns, pass/fail outcomes, and the APFD (Average Percentage of Faults Detected) metric — a standard measure of how early failures were detected.

After accumulating at least 5 runs with failures, use the optimize command to find weights that maximise APFD via genetic algorithm:

java -jar test-order-core-jar-with-dependencies.jar optimize .test-order/state.lz4

Or use the Maven plugin goal:

mvn test-order:optimize

The command optimises weights and saves them directly into the state file. Subsequent test runs will automatically load the optimised weights.

Re-run periodically as your project's failure patterns evolve.

If your test suite rarely or never fails in CI: The optimizer needs failure data to work with — it cannot improve APFD without observed failures. In this case stick with the defaults (or the manual tuning table above). You can still benefit from test-order's ordering: tests that cover recently changed code run first even when they pass, giving faster feedback on change-related behaviour before the rest of the suite finishes.

Instrumentation Modes

Three modes trade index precision against learn-run overhead:

ModeRecordsOverhead†Notes
CLASSMethod/constructor entries + foreign static-field accesses~13%Lighter learns; good starting point
METHODCLASS + per-test-method dependency tracking~11%Enables method-level overlap scoring
MEMBER (default)METHOD + exact method/field access per test~13%Highest precision; ~2× index size

†Overhead on spring-petclinic learn runs (baseline 4.93 s, 5 runs each):

ModeMeanMedianStd dev
none4.93 s4.97 s0.21 s
CLASS5.55 s5.67 s0.19 s
METHOD5.47 s5.44 s0.11 s
MEMBER5.57 s5.45 s0.28 s

Overhead only applies to learn runs. Normal ordered test execution (auto mode) adds no instrumentation cost. Re-learn after major refactors or on a weekly CI schedule — not every build.

Change Detection Modes

ModeWhen to useSource of truth
uncommitted (default)Local iteration — catch your current editsStaged + unstaged + untracked files
since-last-runLocal iteration without gitLZ4 hash snapshots (.test-order/hashes.lz4)
since-last-commitCI / branch workflowsgit diff HEAD~1..HEAD + uncommitted overlay
explicitScripted targeting-Dtestorder.changed.classes=pkg.Foo,pkg.Bar

auto selects: since-last-run (if a hash snapshot exists) → since-last-commit.

Override with -Dtestorder.changeMode=<mode>.

Package Detection

The plugin automatically scans src/main/java to detect the top-level source packages and uses them as the instrumentation filter. This means zero configuration is needed in most cases — the agent instruments exactly the classes that live in your project.

If you need to instrument additional packages (e.g. a library you want to track), use includePackages — the specified prefixes are merged with the auto-detected source packages:

mvn test -Dtestorder.mode=learn -Dtestorder.includePackages=org.lib.extra,com.other

Redundant prefixes are automatically removed: if both com.example and com.example.app appear, only com.example is kept.

When no source directories exist (e.g. a BOM-only project) and filterByGroupId=true (default), the Maven groupId is used as a fallback.

Method-level Scoring

Opt-in reordering of test methods within each class. Enable with -Dtestorder.methodOrder.enabled=true or <methodOrder>true</methodOrder> in plugin config.

The method score uses the same exponential-decay failure model as class scoring, with class-local speed medians:

SignalDefault weight
New method (no telemetry history)5.0
Changed method3.0
Failure recency3.0
Dep overlap2.0
Speed bonus1.0
Speed penalty−1.0

Speed thresholds are class-local — each method's duration is compared to the median of all methods within its own class.

Fine-tune individual weights via testorder.method.score.<signal> (e.g. testorder.method.score.failureRecency=5).

ML-Enhanced Scoring

When ML predictions are available (testorder.ml.enabled=true and 5+ recorded runs), the show and dashboard goals display a P(fail) column alongside the standard scoring.

ML predictions complement the deterministic scoring system — they do not replace it. The standard score determines test execution order, while P(fail) provides an additional signal about test reliability over time.

See MAVEN_PLUGIN.md for configuration details.

Selective Learn (testorder.learn.selective)

When testorder.learn.selective=true, the learn agent only re-instruments classes reachable from the current changes: changed classes plus their transitive callees up to 4 hops in the static call graph. This keeps per-run instrumentation overhead proportional to the size of your change rather than the size of the project.

# Maven
mvn test -Dtestorder.mode=learn -Dtestorder.learn.selective=true

# Gradle
./gradlew test -Dtestorder.mode=learn -Dtestorder.learn.selective=true

Combine with alwaysLearn for low-overhead background index maintenance on every ordered run:

mvn test -Dtestorder.auto.alwaysLearn=true -Dtestorder.learn.selective=true
./gradlew test -Dtestorder.auto.alwaysLearn=true -Dtestorder.learn.selective=true

Both flags default to false. When alwaysLearn=true and no structural changes are detected (empty uncertain set), the agent attach is skipped — zero overhead on no-change runs.

Note: Selective learn is most effective when your change set is small relative to the total project size. For large-scale refactors, a full learn run (selective=false) produces a more complete dependency index.