# Testing in Lux This guide explains how to write and run tests for Lux programs. ## Native Test Effect Lux provides a built-in `Test` effect for writing tests: ```lux fn runTests(): Unit with {Test, Console} = { // Basic assertions Test.assert(1 + 1 == 2, "basic math") // Equality checks Test.assertEqual(List.length([1, 2, 3]), 3, "list length") // Boolean assertions Test.assertTrue(String.contains("hello", "ell"), "contains check") Test.assertFalse(List.isEmpty([1]), "non-empty list") } // Run with the test handler fn main(): Unit with {Console} = { run runTests() with { Test = testReporter } } let result = run main() with {} ``` ### Test Effect Operations | Operation | Purpose | |-----------|---------| | `Test.assert(condition, message)` | Assert condition is true | | `Test.assertEqual(expected, actual, message)` | Assert values are equal | | `Test.assertTrue(condition, message)` | Assert condition is true (alias) | | `Test.assertFalse(condition, message)` | Assert condition is false | ### Running Tests ```bash # Run a test file lux tests/my_tests.lux # Run with the test runner lux test tests/ ``` ## 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 ```bash # Run the full test suite cargo test # Run with output cargo test -- --nocapture ``` ### Running a Single Program ```bash # 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/`: ```lux // 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: ```lux // 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: ```lux // 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): 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`: ```lux 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 ```lux fn findEven(list: List): Option = 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: ```lux fn assertEq(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: ```lux 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: ```bash # 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: ```lux 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: ```bash # 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