Testing

Part of: Reef Language Reference Last reviewed on version: 0.8.0 Status: Best Practices Guide


Overview

Reef provides a built-in test framework through test.framework. The framework uses an Active Object (TestRunner) to track test results with thread-safe state management.


Quick Start

Minimal Test File

import test.framework

proc main()
    let runner = new framework.TestRunner()

    // Your tests here
    runner.assert_eq_int(2 + 2, 4, "basic math works")
    runner.assert_true(true, "true is true")

    runner.report()
end main

Running Tests

# Compile and run
reefc run tests/my_tests.reef

# Or compile then run
reefc tests/my_tests.reef
./tests/my_tests

# Passive-object owner-context tests (opt-in abort on cross-owner access)
reefc tests/my_object_tests.reef --owner-harness

Default builds emit no owner-check probes. See Passive objects — owner-check harness.


The TestRunner

TestRunner is an Active Object that tracks test state:

import test.framework

proc main()
    // Create a new test runner
    let runner = new framework.TestRunner()

    // Run assertions...
    runner.assert_eq_int(1, 1, "one equals one")

    // Print results
    runner.report()
end main

Output:

.
======================
Test Summary
======================
Tests run:    1
Tests passed: 1
Tests failed: 0

All tests passed!

Assertion Methods

assert_eq_int

Compare two integers for equality:

runner.assert_eq_int(actual, expected, "description")

// Examples
runner.assert_eq_int(calculate(), 42, "calculation returns 42")
runner.assert_eq_int(len("hello"), 5, "hello has 5 characters")
runner.assert_eq_int(count, 0, "count starts at zero")

assert_eq_bool

Compare two booleans for equality:

runner.assert_eq_bool(actual, expected, "description")

// Examples
runner.assert_eq_bool(is_valid(), true, "input is valid")
runner.assert_eq_bool(is_empty(list), false, "list is not empty")
runner.assert_eq_bool(a && b, true, "both conditions met")

assert_eq_string

Compare two strings for equality:

runner.assert_eq_string(actual, expected, "description")

// Examples
runner.assert_eq_string(greet("World"), "Hello, World!", "greeting format")
runner.assert_eq_string(trim("  hi  "), "hi", "trim removes whitespace")
runner.assert_eq_string(to_upper("abc"), "ABC", "uppercase conversion")

assert_true

Assert that a condition is true:

runner.assert_true(condition, "description")

// Examples
runner.assert_true(x > 0, "x is positive")
runner.assert_true(contains(list, item), "list contains item")
runner.assert_true(is_valid(input), "input passes validation")

assert_false

Assert that a condition is false:

runner.assert_false(condition, "description")

// Examples
runner.assert_false(is_empty(data), "data is not empty")
runner.assert_false(has_errors(), "no errors occurred")
runner.assert_false(x < 0, "x is not negative")

assert_eq_float

Compare two floats for equality within an epsilon tolerance:

runner.assert_eq_float(actual, expected, epsilon, "description")

// Examples
runner.assert_eq_float(pi(), 3.14159, 0.0001, "pi approximation")
runner.assert_eq_float(sqrt(2.0), 1.414, 0.001, "sqrt(2) approximation")
runner.assert_eq_float(0.1 + 0.2, 0.3, 0.0001, "floating point addition")

Why epsilon? Floating point arithmetic can produce tiny rounding errors. Instead of exact equality, we check if the difference is within an acceptable tolerance.

assert_not_eq_int

Assert that two integers are NOT equal:

runner.assert_not_eq_int(actual, unexpected, "description")

// Examples
runner.assert_not_eq_int(get_id(), 0, "ID is not zero")
runner.assert_not_eq_int(count, -1, "count is not error value")
runner.assert_not_eq_int(result, old_value, "value has changed")

assert_not_eq_bool

Assert that two booleans are NOT equal:

runner.assert_not_eq_bool(actual, unexpected, "description")

// Examples
runner.assert_not_eq_bool(is_ready(), was_ready, "state has changed")
runner.assert_not_eq_bool(flag, false, "flag is set")

assert_not_eq_string

Assert that two strings are NOT equal:

runner.assert_not_eq_string(actual, unexpected, "description")

// Examples
runner.assert_not_eq_string(get_name(), "", "name is not empty")
runner.assert_not_eq_string(hash, old_hash, "hash has changed")
runner.assert_not_eq_string(output, input, "transformation applied")

assert_contains_string

Assert that a haystack string contains a needle substring:

runner.assert_contains_string(haystack, needle, "description")

// Examples
runner.assert_contains_string(error_message(), "invalid", "error mentions invalid input")
runner.assert_contains_string(log_output(), "started", "log confirms startup")

Note: An empty needle is always "contained" (the empty string is a substring of every string, per core.str.contains semantics). The assertion passes in that case, but prints a WARN: assert_contains_string with empty needle notice so an accidentally-empty needle doesn't slip past review as a silently-vacuous pass.


Test Output

Passing Tests

Each passing assertion prints a dot:

...

Failing Tests

Failing assertions print details:

FAIL: expected sum to be 10
  Expected: 10
  Actual: 9

Final Report

Call runner.report() at the end:

======================
Test Summary
======================
Tests run:    15
Tests passed: 14
Tests failed: 1

Some tests failed.

Organizing Tests

Single Test File

For small projects, one test file works:

// tests/all_tests.reef
import test.framework
import myapp.math
import myapp.strings

proc test_math(runner: framework.TestRunner)
    runner.assert_eq_int(add(2, 3), 5, "add works")
    runner.assert_eq_int(multiply(4, 5), 20, "multiply works")
end test_math

proc test_strings(runner: framework.TestRunner)
    runner.assert_eq_string(reverse("abc"), "cba", "reverse works")
end test_strings

proc main()
    let runner = new framework.TestRunner()

    println("=== Math Tests ===")
    test_math(runner)

    println("")
    println("=== String Tests ===")
    test_strings(runner)

    println("")
    runner.report()
end main

Multiple Test Files

For larger projects, separate test files by module:

myproject/
├── src/
│   ├── main.reef
│   ├── math.reef
│   └── strings.reef
└── tests/
    ├── test_math.reef
    └── test_strings.reef

Run each test file separately:

reefc run tests/test_math.reef
reefc run tests/test_strings.reef

Or create a test runner script:

#!/bin/bash
for test in tests/*.reef; do
    echo "Running $test..."
    reefc run "$test"
    echo ""
done

Testing Patterns

proc test_string_operations(runner: framework.TestRunner)
    println("--- String Operations ---")

    // Length tests
    runner.assert_eq_int(len(""), 0, "empty string length")
    runner.assert_eq_int(len("hello"), 5, "hello length")

    // Concatenation tests
    runner.assert_eq_string(concat("a", "b"), "ab", "concat two strings")

    // Trim tests
    runner.assert_eq_string(trim("  x  "), "x", "trim whitespace")
end test_string_operations

Pattern 2: Test Edge Cases

proc test_division(runner: framework.TestRunner)
    // Normal cases
    runner.assert_eq_int(divide(10, 2), 5, "10 / 2 = 5")
    runner.assert_eq_int(divide(7, 3), 2, "7 / 3 = 2 (integer division)")

    // Edge cases
    runner.assert_eq_int(divide(0, 5), 0, "0 / 5 = 0")
    runner.assert_eq_int(divide(5, 5), 1, "5 / 5 = 1")

    // Negative numbers
    runner.assert_eq_int(divide(-10, 2), -5, "-10 / 2 = -5")
    runner.assert_eq_int(divide(10, -2), -5, "10 / -2 = -5")
end test_division

Pattern 3: Test with Setup

import core.option

proc test_stack(runner: framework.TestRunner)
    // Setup
    let stack = new Stack()

    // Test initial state
    runner.assert_true(stack.is_empty(), "new stack is empty")
    runner.assert_eq_int(stack.size(), 0, "new stack has size 0")

    // Test push
    stack.push(42)
    runner.assert_false(stack.is_empty(), "stack not empty after push")
    runner.assert_eq_int(stack.size(), 1, "size is 1 after push")

    // Test pop - returns Option[int]: Some(elem) if non-empty, None if empty
    let value = stack.pop()
    runner.assert_true(option.is_some(value), "pop returns a value")
    runner.assert_eq_int(option.unwrap(value), 42, "pop returns pushed value")
    runner.assert_true(stack.is_empty(), "stack empty after pop")
end test_stack

Pattern 4: Test Error Conditions

Fallible stdlib operations return core.result.Result[T, core.error.Error], not sentinel values — test the Result with core.result's is_ok/is_err/ unwrap_ok:

import core.convert
import core.result

proc test_parse_errors(runner: framework.TestRunner)
    // Valid input
    let good = convert.toInt("42")
    runner.assert_true(result.is_ok(good), "valid input succeeds")
    runner.assert_eq_int(result.unwrap_ok(good), 42, "correct value parsed")

    // Invalid input
    let bad = convert.toInt("not a number")
    runner.assert_true(result.is_err(bad), "invalid input fails")

    // Empty input
    let empty = convert.toInt("")
    runner.assert_true(result.is_err(empty), "empty input fails")
end test_parse_errors

Pattern 5: Test Active Objects

proc test_counter(runner: framework.TestRunner)
    let counter = new Counter()

    // Initial state
    runner.assert_eq_int(counter.get(), 0, "counter starts at 0")

    // Increment
    counter.increment()
    runner.assert_eq_int(counter.get(), 1, "counter is 1 after increment")

    // Multiple increments
    counter.increment()
    counter.increment()
    runner.assert_eq_int(counter.get(), 3, "counter is 3 after 3 increments")

    // Reset
    counter.reset()
    runner.assert_eq_int(counter.get(), 0, "counter is 0 after reset")
end test_counter

Complete Example

A full test file for a math module:

// tests/test_math.reef
import test.framework
import myapp.math

proc test_basic_arithmetic(runner: framework.TestRunner)
    println("--- Basic Arithmetic ---")

    // Addition
    runner.assert_eq_int(add(0, 0), 0, "0 + 0 = 0")
    runner.assert_eq_int(add(1, 2), 3, "1 + 2 = 3")
    runner.assert_eq_int(add(-1, 1), 0, "-1 + 1 = 0")

    // Subtraction
    runner.assert_eq_int(subtract(5, 3), 2, "5 - 3 = 2")
    runner.assert_eq_int(subtract(3, 5), -2, "3 - 5 = -2")

    // Multiplication
    runner.assert_eq_int(multiply(6, 7), 42, "6 * 7 = 42")
    runner.assert_eq_int(multiply(0, 100), 0, "0 * 100 = 0")
    runner.assert_eq_int(multiply(-3, 4), -12, "-3 * 4 = -12")
end test_basic_arithmetic

proc test_power(runner: framework.TestRunner)
    println("--- Power Function ---")

    runner.assert_eq_int(power(2, 0), 1, "2^0 = 1")
    runner.assert_eq_int(power(2, 1), 2, "2^1 = 2")
    runner.assert_eq_int(power(2, 8), 256, "2^8 = 256")
    runner.assert_eq_int(power(10, 3), 1000, "10^3 = 1000")
end test_power

proc test_absolute_value(runner: framework.TestRunner)
    println("--- Absolute Value ---")

    runner.assert_eq_int(abs(5), 5, "abs(5) = 5")
    runner.assert_eq_int(abs(-5), 5, "abs(-5) = 5")
    runner.assert_eq_int(abs(0), 0, "abs(0) = 0")
end test_absolute_value

proc test_min_max(runner: framework.TestRunner)
    println("--- Min/Max ---")

    runner.assert_eq_int(min(3, 7), 3, "min(3, 7) = 3")
    runner.assert_eq_int(min(7, 3), 3, "min(7, 3) = 3")
    runner.assert_eq_int(min(5, 5), 5, "min(5, 5) = 5")

    runner.assert_eq_int(max(3, 7), 7, "max(3, 7) = 7")
    runner.assert_eq_int(max(7, 3), 7, "max(7, 3) = 7")
    runner.assert_eq_int(max(5, 5), 5, "max(5, 5) = 5")
end test_min_max

proc main()
    println("========================================")
    println("Math Module Tests")
    println("========================================")
    println("")

    let runner = new framework.TestRunner()

    test_basic_arithmetic(runner)
    println("")

    test_power(runner)
    println("")

    test_absolute_value(runner)
    println("")

    test_min_max(runner)
    println("")

    runner.report()
end main

Best Practices

1. Write Descriptive Messages

// Bad: Vague
runner.assert_eq_int(result, 5, "test 1")

// Good: Descriptive
runner.assert_eq_int(result, 5, "add(2, 3) returns 5")

2. Test One Thing Per Assertion

// Bad: Testing multiple things
runner.assert_true(x > 0 && y > 0 && z > 0, "all positive")

// Good: Separate assertions
runner.assert_true(x > 0, "x is positive")
runner.assert_true(y > 0, "y is positive")
runner.assert_true(z > 0, "z is positive")

3. Test Both Success and Failure Cases

// Test success
runner.assert_true(is_valid("good@email.com"), "valid email accepted")

// Test failure cases
runner.assert_false(is_valid(""), "empty string rejected")
runner.assert_false(is_valid("no-at-sign"), "missing @ rejected")
runner.assert_false(is_valid("@no-local"), "missing local part rejected")

4. Use Consistent Naming

// File: test_<module>.reef
// Procedure: test_<feature>(runner: framework.TestRunner)

proc test_string_length(runner: framework.TestRunner)
    // ...
end test_string_length

proc test_string_concat(runner: framework.TestRunner)
    // ...
end test_string_concat

5. Print Section Headers

proc main()
    let runner = new framework.TestRunner()

    println("=== Unit Tests ===")
    println("")

    println("--- String Tests ---")
    test_strings(runner)
    println("")

    println("--- Math Tests ---")
    test_math(runner)
    println("")

    runner.report()
end main

Assertion Summary

Method Purpose
assert_eq_int Two integers are equal
assert_eq_bool Two booleans are equal
assert_eq_string Two strings are equal
assert_eq_float Two floats are equal within epsilon
assert_true Condition is true
assert_false Condition is false
assert_not_eq_int Two integers are NOT equal
assert_not_eq_bool Two booleans are NOT equal
assert_not_eq_string Two strings are NOT equal
assert_contains_string Haystack string contains needle substring

Future Improvements

Planned enhancements to the test framework:

  1. assert_throws - Test that code produces expected errors
  2. Test discovery - Automatic test file detection
  3. Timing - Report test execution time
  4. Parallel execution - Run tests concurrently
  5. Fixtures - Setup/teardown hooks

Previous: 075_MODULES.md Next: 100_UNSAFE.md