Home › Developer Docs › Testing Guide
Aether Testing Guide
This document provides comprehensive guidance on testing the Aether interpreter.
Table of Contents
- Test Organization
- Running Tests
- Test-Driven Development
- Writing Tests
- Debugging Test Failures
- Test Coverage Goals
Test Organization
Directory Structure
Integration tests are one file per feature in tests/, named
<feature>_test.rs. Unit tests live beside the code they cover, named
<module>_tests.rs (note the plural — that suffix is what distinguishes them):
src/
├── lexer/
│ └── lexer_tests.rs # Lexer unit tests
├── parser/
│ └── parser_tests.rs # Parser unit tests
└── interpreter/
├── interpreter_tests.rs # Interpreter unit tests
└── builtins_tests.rs # Built-ins unit tests
To list the integration suites and their sizes:
ls tests/
cargo test -- --test-threads=1 # per-suite counts in the output
Test Categories
Unit tests — individual components in isolation, in src/**/*_tests.rs.
Fast (< 1 second), no inter-test dependencies. Run as unittests src/lib.rs.
Integration tests — complete programs end-to-end, in tests/. These exercise
feature interaction and are where new features get their coverage.
Counts are deliberately not listed here; see ARCHITECTURE.md — Current Status for the last measured total and the command to re-measure.
Running Tests
Basic Commands
# Run all tests
cargo test
# Run specific test
cargo test test_name
# Run tests for specific module
cargo test lexer
cargo test parser
cargo test interpreter
# Show println! output during tests
cargo test -- --nocapture
Recommended Options
IMPORTANT: Always use these flags to prevent memory issues:
# Sequential execution (prevents memory pressure)
cargo test -- --test-threads=1
# With output
cargo test -- --nocapture --test-threads=1
# Single test with output
cargo test test_name -- --nocapture --test-threads=1
Why --test-threads=1?
- Reduces memory pressure (parallel tests can use 135 GB+)
- More predictable execution
- Easier debugging
Test Filtering
# Run only integration tests
cargo test --test integration_tests
# Run only unit tests
cargo test --lib
# Run tests matching pattern
cargo test string # Runs all tests with "string" in name
# Run ignored tests
cargo test -- --ignored
Continuous Testing
# Watch for changes and re-run tests
cargo watch -x test
# Watch with clear screen
cargo watch -c -x "test -- --test-threads=1"
Test-Driven Development
The Red-Green-Refactor Cycle
1. Red: Write a failing test
#[test]
fn test_exponentiation() {
let result = eval("2 ** 3");
assert_eq!(result, Value::Int(8)); // FAILS - feature not implemented
}
2. Green: Write minimal code to make it pass
// Add just enough code to pass the test
// Don't over-engineer!
3. Refactor: Improve code while keeping tests green
// Clean up implementation
// Tests should still pass
TDD Workflow Example
Goal: Add string repeat() method
Step 1: Write the test first
#[test]
fn test_string_repeat() {
let result = eval("\"ha\".repeat(3)");
assert_eq!(result, Value::string("hahaha".to_string()));
}
Step 2: Run test (it should fail)
cargo test test_string_repeat
# Expected failure: repeat not implemented
Step 3: Implement minimum code to pass
// In stdlib/string.ae
fn repeat(text, n) {
let result = ""
let i = 0
while (i < n) {
result = result + text
i = i + 1
}
return result
}
Step 4: Run test again (should pass)
cargo test test_string_repeat -- --test-threads=1
Step 5: Add edge case tests
#[test]
fn test_string_repeat_zero() {
assert_eq!(eval("\"x\".repeat(0)"), Value::string("".to_string()));
}
#[test]
fn test_string_repeat_negative() {
assert!(eval("\"x\".repeat(-1)").is_err());
}
Step 6: Refactor if needed
Benefits of TDD
✅ Clear requirements - Test defines what “done” means ✅ Confidence - Refactoring doesn’t break functionality ✅ Documentation - Tests show how to use features ✅ Better design - Testable code is usually better code ✅ Regression prevention - Old tests catch new bugs
Writing Tests
Unit Test Structure
#[test]
fn test_<component>_<feature>() {
// Arrange: Set up test data
let input = "...";
// Act: Execute the operation
let result = operation(input);
// Assert: Verify the result
assert_eq!(result, expected);
}
Integration Test Structure
#[test]
fn test_<feature_name>() {
let source = r#"
fn main() {
// Aether code here
}
"#;
let result = run_program(source);
assert_eq!(result, expected_output);
}
Assertion Helpers
// Equality
assert_eq!(actual, expected);
assert_ne!(actual, unexpected);
// Boolean
assert!(condition);
assert!(!condition);
// Error handling
assert!(result.is_ok());
assert!(result.is_err());
// Pattern matching
match result {
Ok(Value::Int(n)) => assert_eq!(n, 42),
_ => panic!("Expected Int"),
}
Test Naming Conventions
Good test names:
test_addition_integerstest_division_by_zero_errortest_array_push_increases_lengthtest_string_upper_ascii
Poor test names:
test1,test2test_functiontest_it_works
Pattern: test_<what>_<scenario>_<expected_result>
Debugging Test Failures
Step 1: Read the Error Message
---- test_division_by_zero panicked at 'assertion failed: `(left == right)`
left: `Ok(Int(5))`,
right: `Err(DivisionByZero)`', tests/integration_test.rs:42:5
Key information:
- Test name:
test_division_by_zero - Expected:
Err(DivisionByZero) - Actual:
Ok(Int(5)) - Location:
tests/integration_test.rs:42:5
Step 2: Isolate the Test
# Run only the failing test
cargo test test_division_by_zero -- --nocapture --test-threads=1
Step 3: Add Debug Output
#[test]
fn test_division_by_zero() {
let source = "10 / 0";
println!("Input: {}", source);
let result = eval(source);
println!("Result: {:?}", result);
assert!(result.is_err());
}
Step 4: Use Rust Debugger
# Install rust-lldb or rust-gdb
rust-lldb target/debug/deps/aether-<hash>
# Set breakpoint
(lldb) breakpoint set --name test_division_by_zero
(lldb) run
# Step through
(lldb) step
(lldb) print variable_name
Common Test Failures
Memory Issues
error: test failed, to rerun pass '--lib'
signal: 9, SIGKILL: kill
Solution: Use --test-threads=1
Timeout
test hangs indefinitely
Solution:
- Check for infinite loops
- Add timeout to test command
- Use
timeout 60 cargo test
Floating Point Precision
// Wrong: Exact equality
assert_eq!(result, 3.14159);
// Right: Approximate equality
assert!((result - 3.14159).abs() < 0.00001);
String/Array Comparison
// For Rc-wrapped values, use pattern matching or helper methods
match &value {
Value::String(s) => assert_eq!(s.as_ref(), "expected"),
_ => panic!("Expected string"),
}
Test Coverage Goals
Current Coverage
The suite is expected to be fully green — 0 failed, 0 ignored. A new #[ignore]
needs a comment saying why and what would un-ignore it.
For the current total, see ARCHITECTURE.md — Current Status.
Coverage by Component
Every shipped feature has a dedicated suite. ls tests/ is the authoritative
list; the file name maps to the feature (tcp_test.rs → TCP, enum_test.rs →
enums, plugin_v2_test.rs → the V2 FFI protocol).
A feature without a tests/<feature>_test.rs file is not considered shipped —
see the Post-Feature Checklist.
What to Test
Always test:
- ✅ Happy path (valid inputs)
- ✅ Edge cases (empty, zero, null)
- ✅ Error conditions (invalid inputs)
- ✅ Boundary values (min, max)
- ✅ Type mismatches
Example - Testing array.push():
// Happy path
test_array_push_adds_element()
// Edge cases
test_array_push_to_empty_array()
test_array_push_multiple_types()
// Integration
test_array_push_in_loop()
test_array_push_with_function_result()
Measuring Coverage
# Install tarpaulin
cargo install cargo-tarpaulin
# Run coverage report
cargo tarpaulin --out Html
# Open report
open tarpaulin-report.html
Coverage Goals:
- Core components: 90%+ line coverage
- Error handling paths: 80%+ coverage
- Overall: 85%+ coverage
Best Practices
Do’s ✅
- Write tests first (TDD)
- Test one thing per test
- Use descriptive names
- Test edge cases
- Keep tests independent
- Use
--test-threads=1for Aether tests - Commit tests with code
Don’ts ❌
- Don’t test implementation details
- Don’t share state between tests
- Don’t skip error cases
- Don’t use random values (unless testing random behavior)
- Don’t make tests depend on each other
- Don’t ignore failing tests (fix or remove)
Test Smells
Problem: Tests are slow (> 10 seconds)
- Solution: Mock external dependencies, reduce test scope
Problem: Tests are flaky (pass/fail randomly)
- Solution: Check for race conditions, shared state, random values
Problem: Tests break on every change
- Solution: Test behavior, not implementation details
Problem: Can’t understand what test does
- Solution: Better names, comments, simpler test structure
Continuous Integration
Future CI Setup
When setting up CI, include:
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: Run tests
run: cargo test -- --test-threads=1
- name: Check formatting
run: cargo fmt --check
- name: Run clippy
run: cargo clippy -- -D warnings
Resources
Internal Documentation
- DEVELOPMENT.md - Development workflow
CLAUDE.md(repo root) - Project overview- Component docs: LEXER.md, PARSER.md, INTERPRETER.md
External Resources
Last Updated: July 29, 2026 Phase: 5 Complete