Files
lux/docs/testing.md
Brandon Lucas 730112a917 feat: add stress test projects and testing documentation
- 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>
2026-02-13 18:53:27 -05:00

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 parsing
  • projects/todo-app/main.lux - Tests ADTs and list operations
  • projects/mini-interpreter/main.lux - Tests complex pattern matching
  • projects/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

  1. One concern per test - Each test should verify one specific behavior
  2. Descriptive names - Use clear test function names that describe what's being tested
  3. Expected output - Create .expected files for reproducible testing
  4. Test edge cases - Include tests for empty lists, zero values, error conditions
  5. Keep tests fast - Avoid complex computations in tests
  6. Test effects separately - Mock effects to isolate behavior being tested