Testing
Overview
Frankie has a built-in test runner — no external libraries, no configuration.
Write assertions in any .fk file and run it with frankiec test.
wEvery assertion prints a ✓ or ✗ in real time, and the runner exits with code 1 if any test fails, making it CI-friendly.
Running Tests
frankiec test # runs test.fk in the current directory
frankiec test mytest.fk # runs a specific file
Output looks like:
╔══ Frankie Test Runner ════════════════════════════════
║ test.fk
╠═══════════════════════════════════════════════════════
✓ addition works
✓ mean of [1,2,3] is 2.0
✗ expected 4, got 5
✓ FileNotFoundError raised on missing file
╠═══════════════════════════════════════════════════════
║ 3/4 passed, 1 failed (12.3ms)
╚═══════════════════════════════════════════════════════
Assertions
All assertion functions are available without any import inside a test file.
assert_eq(actual, expected, msg)
Equality
The workhorse. Passes if actual == expected.
assert_eq(2 + 2, 4, "addition")
assert_eq(mean([1,2,3]), 2.0, "mean of 1..3")
assert_eq("hello".upcase, "HELLO", "upcase")
assert_eq([1,2,3].length, 3, "vector length")
assert_neq(actual, expected, msg)
Inequality
Passes if actual != expected.
assert_neq(rand_int(1, 100), 0, "rand_int never returns 0")
assert_neq("hello", "", "non-empty string")
assert_true(value, msg)
Truthy
Passes if value is truthy (anything except false and nil).
assert_true(file_exists("/tmp"), "tmp dir exists")
assert_true([1,2,3].include?(2), "include? works")
assert_true("hello".start_with?("he"), "start_with?")
assert_true(5 > 3, "comparison")
assert_nil(value, msg)
Nil check
Passes if value is nil.
assert_nil(find_user(999), "missing user returns nil")
assert_nil([1,2,3].find do |x| x > 9 end, "find with no match")
assert_nil({a: 1}["missing"], "missing hash key is nil")
assert_match(value, pattern, msg)
Regex match
Passes if pattern matches anywhere in value.
assert_match("alice@example.com", "\\w+@\\w+\\.\\w+", "valid email")
assert_match("2024-03-15", "^\\d{4}-\\d{2}-\\d{2}$", "ISO date")
assert_match("Error: timeout", "timeout", "error message contains timeout")
assert_raises(fn, msg)
Any error raised
Passes if calling fn raises any error.
assert_raises(def()
x = 1 // 0
end, "division by zero raises")
assert_raises(def()
file_read("/no/such/file.txt")
end, "missing file raises")
assert_raises_typed(fn, type, msg)
Specific error type
Passes if calling fn raises exactly the specified error type.
assert_raises_typed(def()
x = 1 // 0
end, "ZeroDivisionError", "division raises ZeroDivisionError")
assert_raises_typed(def()
file_read("/no/such/file.txt")
end, "FileNotFoundError", "missing file raises FileNotFoundError")
assert_raises_typed(def()
"hello" + 42
end, "TypeError", "string + int raises TypeError")
Supported type strings: "ZeroDivisionError", "FileNotFoundError", "TypeError", "ValueError", "IndexError", "KeyError", "NameError", "AttributeError", "RuntimeError", "Exception".
Structuring Tests
There is no required structure — assertions anywhere in the file are collected and reported together. For readability, group related assertions with comments:
# test.fk
# ── Math ──────────────────────────────────────────────────────
assert_eq(abs(-5), 5, "abs negative")
assert_eq(abs(5), 5, "abs positive")
assert_eq(round(3.14159,2), 3.14, "round to 2dp")
assert_eq(clamp(15, 0, 10), 10, "clamp above max")
assert_eq(clamp(-5, 0, 10), 0, "clamp below min")
# ── String ────────────────────────────────────────────────────
assert_eq("hello".upcase, "HELLO", "upcase")
assert_eq(" hi ".strip, "hi", "strip")
assert_eq("a,b,c".split(",").length, 3, "split count")
assert_match("frank123", "\\d+", "contains digits")
# ── Vector ────────────────────────────────────────────────────
v = [3, 1, 4, 1, 5, 9, 2, 6]
assert_eq(v.length, 8, "length")
assert_eq(min(v), 1, "min")
assert_eq(max(v), 9, "max")
assert_eq(v.sort[0], 1, "sort first")
assert_eq(v.uniq.length, 7, "uniq removes one duplicate")
# ── Error handling ────────────────────────────────────────────
assert_raises_typed(def()
1 // 0
end, "ZeroDivisionError", "division by zero")
assert_raises_typed(def()
file_read("/no/such/file.txt")
end, "FileNotFoundError", "missing file")
Testing Your Own Functions
Require your lib files the same way main.fk does:
# test.fk
require "lib/math_utils"
require "lib/string_utils"
# ── math_utils ────────────────────────────────────────────────
assert_eq(circle_area(1), round(3.14159, 2), "unit circle")
assert_eq(circle_area(0), 0.0, "zero radius")
assert_eq(factorial(5), 120, "factorial 5")
assert_eq(factorial(0), 1, "factorial 0")
# ── string_utils ──────────────────────────────────────────────
assert_eq(slugify("Hello World"), "hello-world", "slugify basic")
assert_eq(slugify(" spaces "), "spaces", "slugify trims")
assert_eq(truncate("hello", 3), "hel...", "truncate")
assert_nil(truncate("hi", 10), "no truncate needed returns nil")
Testing File I/O
Use a temporary path and clean up in every test:
path = "/tmp/fk_test_#{rand_int(1000, 9999)}.txt"
# Write and read back
file_write(path, "hello frankie\n")
assert_true(file_exists(path), "file was created")
assert_eq(file_read(path).strip, "hello frankie", "content correct")
# Append
file_append(path, "second line\n")
assert_eq(file_lines(path).length, 2, "two lines after append")
# Delete
file_delete(path)
assert_true(not file_exists(path), "file deleted")
# FileNotFoundError
assert_raises_typed(def()
file_read("/no/such/file.txt")
end, "FileNotFoundError", "missing file raises correctly")
Watch Mode
Run tests automatically whenever you save:
frankiec watch test.fk --test
Every time you save test.fk (or any file, since watch triggers on the target file), the test suite re-runs.
Combine with require to automatically pick up changes in your lib files too.
Quick Reference
| Assertion | Passes when |
|---|---|
assert_eq(actual, expected, msg) |
actual == expected |
assert_neq(actual, expected, msg) |
actual != expected |
assert_true(value, msg) |
value is truthy |
assert_nil(value, msg) |
value is nil |
assert_match(value, pattern, msg) |
pattern matches anywhere in value |
assert_raises(fn, msg) |
calling fn raises any error |
assert_raises_typed(fn, type, msg) |
calling fn raises that specific type |