Mojo Testing
Testing verifies that your code produces correct results across a range of inputs — before you deploy it or share it with others. Well-tested code catches regressions (bugs introduced by later changes) immediately and gives you confidence to refactor aggressively. Mojo's testing module provides assertion functions that form the building blocks of a complete test suite.
The Safety Net Analogy
Code without tests: Code with tests:
┌─────────────────┐ ┌─────────────────┐
│ You change │ │ You change │
│ something │ │ something │
└────────┬────────┘ └────────┬────────┘
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ Hope nothing │ │ Run tests │
│ broke │ │ in seconds │
│ (find out in │ └────────┬────────┘
│ production) │ │
└─────────────────┘ ┌────────┴────────┐
│ │
Pass Fail
(safe to (fix now,
ship) not later)
The Testing Module
from testing import (
assert_true,
assert_false,
assert_equal,
assert_not_equal,
assert_almost_equal,
)
Writing Your First Test
from testing import assert_equal, assert_true
# The function being tested
fn celsius_to_fahrenheit(c: Float64) -> Float64:
return c * 9.0 / 5.0 + 32.0
# The test function
fn test_celsius_to_fahrenheit() raises:
assert_equal(celsius_to_fahrenheit(0.0), 32.0, "0°C = 32°F")
assert_equal(celsius_to_fahrenheit(100.0), 212.0, "100°C = 212°F")
assert_equal(celsius_to_fahrenheit(-40.0), -40.0, "-40°C = -40°F")
assert_true(celsius_to_fahrenheit(37.0) > 98.0, "body temp above 98°F")
print("test_celsius_to_fahrenheit passed")
fn main() raises:
test_celsius_to_fahrenheit()
Testing Floating-Point Results
Never use assert_equal for floating-point results — tiny rounding differences cause false failures. Use assert_almost_equal with a tolerance instead.
from testing import assert_almost_equal
from math import sqrt
fn test_sqrt() raises:
assert_almost_equal(sqrt(2.0), 1.41421356, atol=1e-6, msg="sqrt(2)")
assert_almost_equal(sqrt(9.0), 3.0, atol=1e-9, msg="sqrt(9)")
print("test_sqrt passed")
Why atol matters: sqrt(2.0) computed = 1.4142135623730951 expected = 1.41421356 difference = 0.0000000023... < 1e-6 → PASS assert_equal would fail because the floats are not bit-for-bit identical. assert_almost_equal passes because the difference is within tolerance.
Organizing Tests into Test Functions
Each test function covers one unit of behavior. Name them clearly so failures identify the problem immediately.
from testing import assert_equal, assert_true, assert_false
fn is_prime(n: Int) -> Bool:
if n < 2: return False
var i = 2
while i * i <= n:
if n % i == 0: return False
i += 1
return True
fn test_is_prime_small_primes() raises:
assert_true(is_prime(2), "2 is prime")
assert_true(is_prime(3), "3 is prime")
assert_true(is_prime(5), "5 is prime")
assert_true(is_prime(7), "7 is prime")
assert_true(is_prime(11), "11 is prime")
print("test_is_prime_small_primes passed")
fn test_is_prime_composites() raises:
assert_false(is_prime(1), "1 is not prime")
assert_false(is_prime(4), "4 is not prime")
assert_false(is_prime(9), "9 is not prime")
assert_false(is_prime(15), "15 is not prime")
print("test_is_prime_composites passed")
fn test_is_prime_edge_cases() raises:
assert_false(is_prime(0), "0 is not prime")
assert_false(is_prime(-5), "negative is not prime")
print("test_is_prime_edge_cases passed")
fn main() raises:
test_is_prime_small_primes()
test_is_prime_composites()
test_is_prime_edge_cases()
print("All tests passed!")
Testing Error Conditions
from testing import assert_true
fn divide(a: Float64, b: Float64) raises -> Float64:
if b == 0.0:
raise Error("division by zero")
return a / b
fn test_divide_by_zero_raises() raises:
var error_raised = False
try:
_ = divide(10.0, 0.0)
except e:
error_raised = True
assert_true(str(e).find("division by zero") != -1,
"error message must mention 'division by zero'")
assert_true(error_raised, "divide(10, 0) must raise an error")
print("test_divide_by_zero_raises passed")
Test Coverage: Normal, Edge, Error Cases
For any function, test three categories: Normal cases: → Typical valid inputs the function is designed for → The "happy path" that most callers use Edge cases: → Boundary values: 0, -1, max_int, empty string, single-element list → Off-by-one scenarios: exactly at a limit, one above, one below Error cases: → Invalid inputs that should raise errors → Verify the error message is descriptive → Confirm the function does NOT succeed on bad input
fn test_list_sum() raises:
# Normal
var a = List[Int](1, 2, 3, 4, 5)
assert_equal(list_sum(a), 15, "sum of 1..5")
# Edge: single element
var b = List[Int](42)
assert_equal(list_sum(b), 42, "single element sum")
# Edge: all zeros
var c = List[Int](0, 0, 0)
assert_equal(list_sum(c), 0, "all zeros sum")
# Edge: negative numbers
var d = List[Int](-1, -2, -3)
assert_equal(list_sum(d), -6, "negative sum")
Running Tests Selectively
fn run_all_tests() raises:
test_celsius_to_fahrenheit()
test_is_prime_small_primes()
test_is_prime_composites()
test_is_prime_edge_cases()
test_divide_by_zero_raises()
print("=== All tests passed ===")
fn main() raises:
run_all_tests()
Key Takeaways
Tests are functions that call assertions on your production code. Use assert_equal for exact comparisons and assert_almost_equal with a tolerance for floating-point results. Name test functions clearly so failures pinpoint the problem. Cover three categories for every function: normal inputs, edge cases, and error conditions. Collect all test calls in a run_all_tests function. Run tests after every code change to catch regressions instantly. Tests are not overhead — they are the fastest way to know your code is correct.
