Configuration file
All Visdom Testing settings live in a single visdom-testing.yaml file at the project root.
Each layer can be enabled or disabled independently, and thresholds are configurable per layer.
# visdom-testing.yaml
version: "1.0"
layers:
architecture:
enabled: true
fail-on-violation: true
baseline: archunit_store/ # FreezingArchRule baseline directory
property-based:
enabled: true
default-tries: 1000 # tries per property (override per-test)
seed: fixed # "fixed" for reproducibility, "random" for exploration
timeout-per-property: 10s
mutation:
enabled: true
target-classes:
- "com.example.pricing.*"
- "com.example.billing.*"
excluded-classes:
- "com.example.*.config.*"
- "com.example.*.dto.*"
mutators:
- DEFAULTS # PIT default mutator group
- EXTENDED # additional mutators for thorough analysis
threshold:
minimum-score: 60 # percentage, PR gate
warn-score: 75 # percentage, warning level
contracts:
enabled: true
broker-url: "https://pact-broker.example.com"
publish-results: true
provider-version-from: git-commit
consumer-version-selectors:
- tag: main
- deployed: true
quality-gates:
mutation-score: 60 # minimum mutation score for changed files
architecture-compliance: true # zero ArchUnit violations
contract-verification: true # all contracts must pass
max-flakiness: 2 # percentage, per-run flakiness budget
ci:
pr-scope: changed-files # "changed-files" or "changed-modules"
nightly-scope: full-modules
parallel: true
timeout: 30m
Layer 0: ArchUnit
Test placement
Place your architecture tests in a dedicated test class, typically at
src/test/java/com/example/architecture/ArchitectureTest.java.
Use the @AnalyzeClasses annotation to scope the analysis.
@AnalyzeClasses(packages = "com.example", importOptions = {
ImportOption.DoNotIncludeTests.class
})
class ArchitectureTest {
@ArchTest
static final ArchRule controllers_should_not_access_repositories =
noClasses()
.that().resideInAPackage("..controller..")
.should().accessClassesThat()
.resideInAPackage("..repository..");
@ArchTest
static final ArchRule no_field_injection =
noFields()
.should().beAnnotatedWith("org.springframework.beans.factory.annotation.Autowired");
@ArchTest
static final ArchRule no_generic_exceptions =
noClasses()
.should().callMethodWhere(
JavaCall.Predicates.target(HasName.Predicates.name("throw"))
);
} FreezingArchRule for baselines
When adopting ArchUnit on a legacy codebase, use FreezingArchRule to capture existing
violations as a baseline. New violations fail the build; existing ones are tracked and reduced over time.
@ArchTest
static final ArchRule no_rest_template = FreezingArchRule.freeze(
noClasses()
.should().accessClassesThat()
.haveFullyQualifiedName("org.springframework.web.client.RestTemplate")
);
The baseline is stored in archunit_store/ by default. Commit this directory to version
control so the baseline is shared across the team.
Pre-push hook setup
# .git/hooks/pre-push
#!/bin/sh
echo "Running ArchUnit tests..."
mvn test -pl :architecture-tests -Dtest=ArchitectureTest -q
if [ $? -ne 0 ]; then
echo "ArchUnit violations detected. Push rejected."
exit 1
fi Layer 1: Property-Based Testing (jqwik)
jqwik configuration
jqwik runs as a JUnit 5 test engine. Add the dependency and configure via
jqwik.properties or annotations.
# src/test/resources/jqwik.properties
jqwik.tries.default=1000
jqwik.maxDiscardRatio=5
jqwik.reporting.onlyFailures=true
jqwik.database=.jqwik-database Tries per property
| Context | Tries | Use case |
|---|---|---|
| Development | 100 | Fast feedback during TDD loop |
| CI (per PR) | 1,000 | Standard verification on each push |
| Nightly / release | 10,000 | Thorough exploration for rare edge cases |
Seed management
jqwik records failing seeds in .jqwik-database. When a property fails, jqwik
replays the failing seed on subsequent runs to ensure the bug stays caught until fixed.
Commit .jqwik-database or set a fixed seed in CI for reproducibility.
Layer 2: Mutation Testing (PIT)
PIT configuration
<!-- pom.xml -->
<plugin>
<groupId>org.pitest</groupId>
<artifactId>pitest-maven</artifactId>
<version>1.17.1</version>
<configuration>
<targetClasses>
<param>com.example.pricing.*</param>
<param>com.example.billing.*</param>
</targetClasses>
<excludedClasses>
<param>com.example.*.config.*</param>
<param>com.example.*.dto.*</param>
</excludedClasses>
<mutators>
<mutator>DEFAULTS</mutator>
</mutators>
<timestampedReports>false</timestampedReports>
<outputFormats>
<param>HTML</param>
<param>XML</param>
</outputFormats>
<mutationThreshold>60</mutationThreshold>
</configuration>
</plugin> Target classes
Focus mutation testing on business logic, not infrastructure. Exclude DTOs, configuration classes, and generated code. Mutating these adds CI time without finding real bugs.
Mutator selection
| Mutator group | What it does | When to use |
|---|---|---|
DEFAULTS | Conditionals, math, return values | Always — baseline mutators |
EXTENDED | Adds constructor calls, inline constants, remove conditionals | Nightly runs for deeper analysis |
ALL | Every available mutator | Release gate / audit only |
Thresholds
Set mutationThreshold to your minimum acceptable score. Start at 60% and raise it
as your test suite improves. A score below 60% typically indicates significant gaps in test
effectiveness.
Layer 3: Contract Testing (Pact)
Pact Broker configuration
# visdom-testing.yaml (contracts section)
contracts:
broker-url: "https://pact-broker.example.com"
publish-results: true
provider-version-from: git-commit
consumer-version-selectors:
- tag: main # verify against main branch consumers
- deployed: true # verify against currently deployed consumers Provider verification
@Provider("pricing-service")
@PactBroker(
url = "\$\{pact.broker.url\}",
consumerVersionSelectors = {
@ConsumerVersionSelector(tag = "main"),
@ConsumerVersionSelector(deployed = true)
}
)
class PricingProviderTest {
// Provider state setup and verification
} Contract versioning
Use git commit SHA as the provider version. This ensures each build is uniquely identified and the Pact Broker can track which versions are compatible. Tag versions with the branch name for consumer version selection.
CI Integration
GitHub Actions workflow
# .github/workflows/visdom-testing.yml
name: Visdom Testing
on:
pull_request:
branches: [main]
schedule:
- cron: '0 2 * * *' # nightly at 2 AM
jobs:
architecture-and-properties:
name: L0 Architecture + L1 PBT
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
- name: Run ArchUnit + PBT tests
run: mvn test -pl :architecture-tests,:property-tests -q
mutation:
name: L2 Mutation (changed files)
needs: architecture-and-properties
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
- name: Run PIT on changed modules
run: |
CHANGED=\$(git diff --name-only origin/main...HEAD -- '*.java' | \\
sed 's|src/main/java/||;s|/[^/]*\$||;s|/|.|g' | sort -u)
mvn org.pitest:pitest-maven:mutationCoverage \\
-DtargetClasses="\$\{CHANGED\}" \\
-DmutationThreshold=60
contracts:
name: L3 Contract Verification
needs: architecture-and-properties
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
- name: Verify provider contracts
run: mvn test -pl :contract-tests -q
env:
PACT_BROKER_URL: \$\{\{ secrets.PACT_BROKER_URL \}\}
mutation-nightly:
name: L2 Mutation (full modules)
runs-on: ubuntu-latest
if: github.event_name == 'schedule'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
- name: Run full mutation analysis
run: mvn org.pitest:pitest-maven:mutationCoverage GitLab CI example
# .gitlab-ci.yml
stages:
- fast-checks
- pr-checks
- nightly
archunit-pbt:
stage: fast-checks
script:
- mvn test -pl :architecture-tests,:property-tests -q
rules:
- if: \$CI_PIPELINE_SOURCE == "merge_request_event"
- if: \$CI_PIPELINE_SOURCE == "schedule"
mutation-pr:
stage: pr-checks
script:
- >
CHANGED=\$(git diff --name-only \$CI_MERGE_REQUEST_DIFF_BASE_SHA...HEAD -- '*.java' |
sed 's|src/main/java/||;s|/[^/]*\$||;s|/|.|g' | sort -u)
- mvn org.pitest:pitest-maven:mutationCoverage
-DtargetClasses="\$\{CHANGED\}"
-DmutationThreshold=60
rules:
- if: \$CI_PIPELINE_SOURCE == "merge_request_event"
contracts:
stage: pr-checks
script:
- mvn test -pl :contract-tests -q
rules:
- if: \$CI_PIPELINE_SOURCE == "merge_request_event"
- if: \$CI_PIPELINE_SOURCE == "schedule"
mutation-full:
stage: nightly
script:
- mvn org.pitest:pitest-maven:mutationCoverage
rules:
- if: \$CI_PIPELINE_SOURCE == "schedule" Quality gates
Quality gates are configured in the quality-gates section of
visdom-testing.yaml and enforced in CI.
| Gate | Condition | Default |
|---|---|---|
| Mutation score | Changed files must meet minimum mutation score | 60% |
| Architecture compliance | Zero ArchUnit violations | Enabled |
| Contract verification | All consumer-driven contracts pass | Enabled |
| Flakiness budget | Per-run flakiness below threshold | 2% |
✅ Start with warnings, then enforce
When first deploying, set quality gates to warn-only mode. Teams see the metrics without being blocked. After 2-4 weeks, once baselines are established and teams understand the metrics, switch to enforcement mode.