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
Pattern 1: Group Related Tests
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:
- assert_throws - Test that code produces expected errors
- Test discovery - Automatic test file detection
- Timing - Report test execution time
- Parallel execution - Run tests concurrently
- Fixtures - Setup/teardown hooks
Previous: 075_MODULES.md Next: 100_UNSAFE.md