- Add String.fromChar function to convert Char to String - Create four stress test projects demonstrating Lux features: - json-parser: recursive descent parsing with Char handling - markdown-converter: string manipulation and ADTs - todo-app: list operations and pattern matching - mini-interpreter: AST evaluation and environments - Add comprehensive testing documentation (docs/testing.md) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
6.3 KiB
Testing in Lux
This guide explains how to write and run tests for Lux programs.
Test File Structure
Lux uses a simple test framework based on comparing program output against expected results. Tests are organized in the tests/ directory.
Expected Output Files
Each .lux file in examples/ can have a corresponding .expected file in tests/expected/ that contains the expected output:
examples/
hello.lux # The program
tests/
expected/
hello.expected # Expected output
Running Tests
Running All Tests
# Run the full test suite
cargo test
# Run with output
cargo test -- --nocapture
Running a Single Program
# Run a specific file
./result/bin/lux examples/hello.lux
# Or with cargo
cargo run -- examples/hello.lux
Writing Tests
Basic Program Test
Create a program in examples/ and its expected output in tests/expected/:
// examples/my_test.lux
fn main(): Unit with {Console} = {
Console.print("Hello, Test!")
Console.print("1 + 2 = " + toString(1 + 2))
}
let output = run main() with {}
Expected output (tests/expected/my_test.expected):
Hello, Test!
1 + 2 = 3
Testing Pure Functions
For testing pure functions, create a test file that prints results:
// examples/test_math.lux
fn add(a: Int, b: Int): Int = a + b
fn multiply(a: Int, b: Int): Int = a * b
fn assertEqual(expected: Int, actual: Int, name: String): Unit with {Console} =
if expected == actual then Console.print(name + ": PASS")
else Console.print(name + ": FAIL - expected " + toString(expected) + ", got " + toString(actual))
fn main(): Unit with {Console} = {
assertEqual(5, add(2, 3), "add(2, 3)")
assertEqual(6, multiply(2, 3), "multiply(2, 3)")
assertEqual(0, add(-1, 1), "add(-1, 1)")
}
let output = run main() with {}
Testing with Effects
Test effect handlers by providing mock implementations:
// Example: Testing with a mock Console
effect Console {
fn print(message: String): Unit
}
// Real implementation would output to terminal
// Test implementation captures output to a list
handler testConsole(output: List<String>): Console {
fn print(message) = {
// In a real test, we'd capture this
resume(())
}
}
fn testableFunction(): Unit with {Console} = {
Console.print("Test output")
}
fn runTest(): Unit with {Console} = {
// Run with test handler
run testableFunction() with {
Console = testConsole([])
}
Console.print("Test completed")
}
let output = run runTest() with {}
Testing Result Types
Use pattern matching to test functions that return Result:
fn testResults(): Unit with {Console} = {
let success = Ok(42)
let failure = Err("Something went wrong")
match success {
Ok(n) => Console.print("Success with " + toString(n)),
Err(e) => Console.print("Unexpected error: " + e)
}
match failure {
Ok(n) => Console.print("Unexpected success"),
Err(e) => Console.print("Expected error: " + e)
}
}
let output = run testResults() with {}
Testing Option Types
fn findEven(list: List<Int>): Option<Int> =
List.find(list, fn(x: Int): Bool => x % 2 == 0)
fn testOptions(): Unit with {Console} = {
match findEven([1, 3, 5]) {
None => Console.print("No even number found: PASS"),
Some(_) => Console.print("FAIL - found unexpected even")
}
match findEven([1, 2, 3]) {
None => Console.print("FAIL - should find 2"),
Some(n) => Console.print("Found even: " + toString(n) + (if n == 2 then " PASS" else " FAIL"))
}
}
let output = run testOptions() with {}
Test Patterns
Assertion Helpers
Create reusable assertion functions:
fn assertEq<T>(expected: T, actual: T, desc: String): Unit with {Console} =
Console.print(desc + ": " + (if expected == actual then "PASS" else "FAIL"))
fn assertTrue(condition: Bool, desc: String): Unit with {Console} =
Console.print(desc + ": " + (if condition then "PASS" else "FAIL"))
fn assertFalse(condition: Bool, desc: String): Unit with {Console} =
Console.print(desc + ": " + (if condition then "FAIL" else "PASS"))
Grouping Tests
Organize related tests into groups:
fn testGroup(name: String): Unit with {Console} = {
Console.print("")
Console.print("=== " + name + " ===")
}
fn runAllTests(): Unit with {Console} = {
testGroup("List Operations")
testListHead()
testListTail()
testListMap()
testGroup("String Operations")
testStringLength()
testStringSplit()
Console.print("")
Console.print("All tests completed")
}
Example Test Files
Look at the example projects for comprehensive test patterns:
projects/json-parser/main.lux- Tests recursive parsingprojects/todo-app/main.lux- Tests ADTs and list operationsprojects/mini-interpreter/main.lux- Tests complex pattern matchingprojects/markdown-converter/main.lux- Tests string manipulation
CI/CD Integration
The test suite runs automatically on every commit. To run locally:
# Build and test
nix build
nix develop --command cargo test
# Test all example programs
for f in examples/standard/*.lux; do
echo "Testing $f"
./result/bin/lux "$f" > /tmp/out.txt 2>&1
if [ $? -eq 0 ]; then
echo " OK"
else
echo " FAILED"
cat /tmp/out.txt
fi
done
Debugging Tests
Verbose Output
Add debug prints to trace execution:
fn debug(msg: String): Unit with {Console} =
Console.print("[DEBUG] " + msg)
fn myFunction(x: Int): Int with {Console} = {
debug("Input: " + toString(x))
let result = x * 2
debug("Output: " + toString(result))
result
}
Type Checking Only
Check types without running:
# Type check only (when available)
./result/bin/lux --check examples/myfile.lux
Best Practices
- One concern per test - Each test should verify one specific behavior
- Descriptive names - Use clear test function names that describe what's being tested
- Expected output - Create
.expectedfiles for reproducible testing - Test edge cases - Include tests for empty lists, zero values, error conditions
- Keep tests fast - Avoid complex computations in tests
- Test effects separately - Mock effects to isolate behavior being tested