Testing Suite¶
MVL's testing philosophy: the compiler handles well-formedness, tests handle validation.
The eleven requirements eliminate entire categories of bugs at compile time. What remains is semantic correctness — does the code do what the specification says? That's what tests verify.
MVL provides five testing mechanisms as language-level features, not bolted-on frameworks.
The Testing Pyramid¶
┌─────────────┐
│ Fuzzing │ ← Find edge cases you didn't imagine
├─────────────┤
│ MC/DC │ ← Prove decision coverage for safety-critical code
├─────────────┤
│ BDD │ ← Validate against specifications
├─────────────┤
│ Property │ ← Prove invariants hold for all inputs
├─────────────┤
│ Unit │ ← Verify individual functions
└─────────────┘
Each layer catches different failure modes. Use all five for critical systems.
1. Unit Tests¶
Internal test functions verify individual functions. They have access to private APIs and live alongside the code they test.
fn add(a: Int, b: Int) -> Int {
a + b
}
test fn test_add_positive() -> Unit {
assert_eq(add(2, 3), 5)
}
test fn test_add_negative() -> Unit {
assert_eq(add(-1, 1), 0)
}
test fn test_add_overflow() -> Unit {
// Refinement types prevent overflow at compile time
// This test verifies the happy path
assert_eq(add(1000000, 1000000), 2000000)
}
Run with mvl test:
$ mvl test src/math.mvl
Running 3 tests...
test_add_positive ... ok
test_add_negative ... ok
test_add_overflow ... ok
3 passed, 0 failed
External Tests¶
Tests in *_test.mvl files test only the public API. They survive code regeneration — when the implementation is regenerated from specs, external tests remain as permanent evidence.
// math_test.mvl — tests public API only
use math.{add}
test fn test_add_public() -> Unit {
assert_eq(add(1, 2), 3)
}
2. Property-Based Testing¶
Property tests verify invariants hold for all inputs, not just examples. MVL generates random inputs and searches for counterexamples.
use std.test.{property, forall}
property fn add_commutative() -> Bool {
forall |a: Int, b: Int| {
add(a, b) == add(b, a)
}
}
property fn add_associative() -> Bool {
forall |a: Int, b: Int, c: Int| {
add(add(a, b), c) == add(a, add(b, c))
}
}
property fn sort_preserves_length() -> Bool {
forall |xs: List[Int]| {
sort(xs).len() == xs.len()
}
}
property fn sort_is_sorted() -> Bool {
forall |xs: List[Int]| {
is_sorted(sort(xs))
}
}
Run with mvl test --property:
$ mvl test --property src/math.mvl
Running 4 property tests (100 iterations each)...
add_commutative ... ok (100/100)
add_associative ... ok (100/100)
sort_preserves_length ... ok (100/100)
sort_is_sorted ... ok (100/100)
4 passed, 0 failed
Shrinking¶
When a property fails, MVL automatically shrinks the counterexample to the minimal failing case:
$ mvl test --property src/buggy.mvl
buggy_property ... FAILED
Counterexample (shrunk): a = 0, b = -1
Original: a = 847291, b = -5829104
3. BDD Scenarios¶
Behavior-Driven Development scenarios express tests in Given-When-Then format, directly mapping to specifications.
use std.test.{scenario, given, when, then}
scenario fn user_login() -> Unit {
given("a registered user with email 'alice@example.com'")
let user: User = create_user("alice@example.com", "password123");
when("they attempt to login with correct credentials")
let result: Result[Session, AuthError] = login("alice@example.com", "password123");
then("they receive a valid session token")
assert(result.is_ok());
assert(result.unwrap().token.len() > 0)
}
scenario fn user_login_wrong_password() -> Unit {
given("a registered user with email 'alice@example.com'")
let user: User = create_user("alice@example.com", "password123");
when("they attempt to login with wrong password")
let result: Result[Session, AuthError] = login("alice@example.com", "wrongpass");
then("they receive an authentication error")
assert(result.is_err());
assert_eq(result.unwrap_err(), AuthError::InvalidCredentials)
}
BDD tests serve as executable specifications. The Given-When-Then structure maps directly to spec requirements, enabling traceability from spec to test to implementation.
4. MC/DC Coverage¶
Modified Condition/Decision Coverage is the most stringent structural coverage metric, required by:
- DO-178C at DAL-A (catastrophic failure in aviation)
- ISO 26262 at ASIL-D (automotive safety)
- EN 50128 at SIL 4 (railway systems)
MC/DC proves that every boolean condition independently affects the decision outcome.
fn authorize(is_admin: Bool, is_owner: Bool, is_public: Bool) -> Bool {
is_admin || (is_owner && is_public)
}
For this function, MC/DC requires test cases showing:
| is_admin | is_owner | is_public | result | Independence shown for |
|---|---|---|---|---|
| T | F | F | T | is_admin |
| F | F | F | F | is_admin |
| F | T | T | T | is_owner |
| F | F | T | F | is_owner |
| F | T | T | T | is_public |
| F | T | F | F | is_public |
Run with mvl mcdc:
$ mvl mcdc src/auth.mvl
MC/DC Analysis
==============
Decision at auth.mvl:2 — 3 clauses (is_admin || (is_owner && is_public))
Coverage: 3/3 clauses have independence pairs
is_admin: (T,F,F)->T vs (F,F,F)->F
is_owner: (F,T,T)->T vs (F,F,T)->F
is_public: (F,T,T)->T vs (F,T,F)->F
PASS: 100% MC/DC coverage
Why MC/DC Matters¶
For safety-critical systems, branch coverage isn't enough. A function like if a && b && c has only two branches (true/false) but eight input combinations. MC/DC proves you've tested the independence of each condition — that flipping any single input can flip the output.
5. Fuzzing¶
Fuzz testing generates random, malformed, and adversarial inputs to find crashes, hangs, and unexpected behavior. MVL's fuzzer is coverage-guided — it tracks which code paths inputs exercise and mutates toward unexplored paths.
use std.fuzz.{fuzz, FuzzInput}
fuzz fn fuzz_parser(input: FuzzInput) -> Unit {
let bytes: List[Byte] = input.bytes(0, 1024);
let text: String = String::from_utf8_lossy(bytes);
// This should never panic, regardless of input
let _ = parse_config(text);
}
fuzz fn fuzz_deserialize(input: FuzzInput) -> Unit {
let json: String = input.ascii_string(0, 4096);
// Deserialization must handle any input gracefully
match deserialize(json) {
Ok(_) => (),
Err(_) => (), // errors are fine, panics are not
}
}
Run with mvl fuzz:
$ mvl fuzz src/parser.mvl --duration 60s
Fuzzing fuzz_parser...
Executions: 1,247,892
Coverage: 847 edges
Crashes: 0
Timeouts: 0
Duration: 60.0s
No issues found.
When to Fuzz¶
Fuzz anything that processes untrusted input:
- Parsers (JSON, XML, config files, protocols)
- Deserializers
- Network handlers
- File format readers
- Compression/decompression
- Cryptographic operations
Test Organization¶
MVL encourages co-locating tests with code:
src/
├── auth.mvl # implementation
├── auth_test.mvl # external tests (public API)
├── parser.mvl
├── parser_test.mvl
└── internal/
└── crypto.mvl # internal module with inline test fns
Internal vs External Tests¶
| Type | Location | Access | Survives regeneration |
|---|---|---|---|
Internal (test fn) |
Same file | Private API | No |
External (*_test.mvl) |
Separate file | Public API only | Yes |
Rule of thumb: Use internal tests for implementation details, external tests for behavior contracts.
Assurance Report¶
mvl assurance generates a report showing test coverage mapped to specification requirements:
$ mvl assurance
Assurance Report
================
Spec: 004-authentication
Requirement 1: User login [MUST]
Implementation: src/auth.mvl::login
Tests:
- auth_test.mvl::test_login_success
- auth_test.mvl::test_login_wrong_password
- auth_test.mvl::scenario_user_login
Coverage: 3 tests, 94% line coverage
Requirement 2: Session expiry [MUST]
Implementation: src/auth.mvl::check_session
Tests:
- auth_test.mvl::test_session_valid
- auth_test.mvl::test_session_expired
- auth_test.mvl::property_session_expiry
Coverage: 3 tests, 100% line coverage
Overall: 12/12 requirements covered, 97% line coverage
This closes the loop: spec requirements → implementation → tests → evidence.