This project uses a comprehensive testing strategy including:
- Unit tests for smart contracts (Rust) and frontend (TypeScript)
- Property-based testing (fuzzing) for contracts
- Code coverage tracking with 95% threshold for contracts
- Mutation testing to verify test suite quality
# Run all contract tests
cargo test --workspace
# Run tests for specific contract
cargo test -p guess-the-number
cargo test -p fungible-allowlist-example
cargo test -p nft-enumerable-example
# Run with output
cargo test -- --nocapture- Tests use Soroban SDK's
testutilsfor mocking and assertions - Each contract has tests in either
src/test.rsortests/test.rs - Tests include mock auth, address generation, and cross-contract calls
cd frontend
npm install# Run tests in watch mode
npm test
# Run tests once
npm test run
# Run with UI
npm run test:ui
# Run with coverage
npm run test:coverage- Component tests:
src/**/*.test.tsx - Utility tests:
src/**/*.test.ts - Setup file:
src/test/setup.ts
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
describe('MyComponent', () => {
it('renders correctly', () => {
render(<MyComponent />);
expect(screen.getByText('Hello')).toBeInTheDocument();
});
});Add to your CI pipeline:
# Smart contracts
- run: cargo test --workspace
# Frontend
- run: cd frontend && npm install && npm test runMutation testing verifies the quality of your test suite by introducing small changes (mutations) to the code and checking if tests catch them. A high mutation score indicates that tests effectively detect bugs.
Mutation testing works by:
- Creating "mutants" — small modifications to your code (e.g., changing
>to>=,+to-) - Running your test suite against each mutant
- Checking if tests fail (mutant "killed") or pass (mutant "survived")
- Calculating a mutation score:
killed / (killed + survived) × 100%
A surviving mutant indicates either:
- Missing test coverage for that code path
- Tests that don't assert the specific behavior being mutated
Installation:
cargo install cargo-mutants --lockedRunning locally:
cd contracts/stellar-save
# Run all mutations (can take 30-60 minutes)
cargo mutants
# Run with parallelism
cargo mutants --jobs 4
# Test specific files only
cargo mutants --file src/penalty.rs --file src/pool.rs
# Show caught mutants (for debugging)
cargo mutants --caughtConfiguration:
- Config file:
contracts/stellar-save/mutants.toml - Excludes: test files, benchmarks, migrations, generated code
- Timeout: 120s per mutant (3x multiplier)
- Target threshold: 60% mutation score
Interpreting results:
Mutation score: 75.0% (45/60 mutants killed)
caught: 45 ← tests detected these mutations ✓
missed: 15 ← tests didn't catch these (need more tests)
timeout: 0 ← mutants that caused infinite loops
unviable: 5 ← mutants that don't compile (skipped)
Adding tests for surviving mutants:
When a mutant survives, examine the mutation and add a test:
// Example: mutant changed `amount > 0` to `amount >= 0`
// Surviving mutant in penalty.rs:
// - if amount <= 0 { return 0; }
// + if amount < 0 { return 0; }
// Add test to catch this:
#[test]
fn test_calculate_penalty_zero_amount() {
let cfg = PenaltyConfig::default();
// This test now catches the >= vs > mutation
assert_eq!(calculate_penalty(0, 3, &cfg), 0);
}Installation:
cd frontend
npm install --save-dev @stryker-mutator/core @stryker-mutator/vitest-runnerRunning locally:
cd frontend
# Run all mutations (can take 20-40 minutes)
npm run test:mutation
# View HTML report
open reports/mutation/mutation.htmlConfiguration:
- Config file:
frontend/stryker.config.mjs - Excludes: test files, assets, i18n, type definitions
- Thresholds: 80% (high), 60% (low), 50% (break/fail)
- Concurrency: 4 workers
Interpreting results:
Mutation score: 68.5% (137/200 mutants killed)
Killed: 137 ← tests caught these ✓
Survived: 63 ← tests missed these (need assertions)
No coverage: 12 ← code not executed by tests
Timeout: 3 ← mutants caused infinite loops
Adding tests for surviving mutants:
// Example: mutant changed `amount > 0` to `amount >= 0`
// Surviving mutant in utils/validation.ts
// Add test to catch this:
it('rejects zero amount', () => {
expect(validateAmount(0)).toBe(false);
// This assertion now catches the > vs >= mutation
});Mutation testing runs automatically:
- On PRs to main/develop (for changed files only)
- Weekly (Sunday 03:00 UTC) for full baseline
- Manual trigger via GitHub Actions (can select scope: all/contracts/frontend)
Workflow: .github/workflows/mutation-testing.yml
Thresholds enforced in CI:
- Contracts: 60% minimum mutation score
- Frontend: 50% minimum mutation score
PR comments: The workflow automatically posts mutation scores as PR comments with:
- Overall score and emoji indicator (🟢 ≥80%, 🟡 ≥60%, 🔴 <60%)
- Breakdown of killed/survived/timeout mutants
- List of surviving mutants (expandable)
- Link to full HTML report in artifacts
-
Start with high-value modules: Focus mutation testing on critical business logic (penalty calculations, pool math, contribution validation)
-
Don't chase 100%: Some mutants are equivalent (produce identical behavior) or test implementation details. Aim for 70-85% on critical modules.
-
Use mutation testing to find gaps: Surviving mutants reveal:
- Missing edge case tests
- Assertions that are too weak
- Dead code that can be removed
-
Combine with coverage: High line coverage + high mutation score = robust test suite
-
Run incrementally: Use
--file(cargo-mutants) ormutatepatterns (Stryker) to test specific modules during development -
Review timeouts: Mutants that timeout often indicate:
- Missing loop termination checks
- Unbounded recursion guards
- Performance-critical code paths
cargo-mutants is slow:
- Use
--jobs Nto parallelize - Use
--fileto test specific modules - Increase
--timeoutif legitimate tests are timing out
Stryker uses too much memory:
- Reduce
concurrencyinstryker.config.mjs - Use
--mutateCLI flag to test specific files - Exclude large generated files
False positives (equivalent mutants):
- Some mutants produce identical behavior (e.g.,
i++vs++iin some contexts) - Document these in code comments or ignore patterns
- Focus on the overall trend, not individual mutants
CI timeout:
- Mutation testing is CPU-intensive; adjust
timeout-minutesin workflow - Consider running full suite only on schedule, not every PR