Testing
Kit has built-in support for testing through test blocks and assertion functions.
Tests run in the interpreter with kit run.
Overview
Kit's testing approach is simple and integrated directly into the language. You write tests
alongside your code using test blocks, and assertions verify expected behavior.
- Test blocks - Define named test cases with the
testkeyword - Assertions - Verify conditions using
assert-*functions - Interpreter execution - Tests run with
kit run, notkit build
test blocks and when running with
the interpreter (kit run). The compiler (kit build) does not compile tests.
Test Blocks
Use the test keyword to define a named test case:
# Basic test block
test "addition works" =>
assert-eq! (1 + 1) 2
# Test with multiple assertions
test "math operations" =>
assert-eq! (2 + 2) 4
assert-eq! (10 - 3) 7
assert-eq! (4 * 5) 20
assert-eq! (15 / 3) 5
# Test with setup
test "list operations" =>
items = [1, 2, 3, 4, 5]
assert-eq! (length items) 5
assert-eq! (head items) 1
assert-eq! (last items) 5
Test Names
Test names should be descriptive and explain what behavior is being tested:
# Good: descriptive names
test "empty list returns None for head" =>
assert-eq! (head []) None
test "filter removes non-matching elements" =>
evens = filter (fn(x) => x % 2 == 0) [1, 2, 3, 4]
assert-eq! evens [2, 4]
test "map transforms each element" =>
doubled = map (fn(x) => x * 2) [1, 2, 3]
assert-eq! doubled [2, 4, 6]
Assertions
Kit provides several assertion functions for different testing needs:
assert-eq!
Assert that two values are equal:
# Basic equality
assert-eq! 42 42
assert-eq! "hello" "hello"
assert-eq! [1, 2, 3] [1, 2, 3]
# Record equality
assert-eq! {x: 1, y: 2} {x: 1, y: 2}
# Result and Option types
assert-eq! (Some 42) (Some 42)
assert-eq! (Ok "success") (Ok "success")
assert-neq!
Assert that two values are not equal:
assert-neq! 1 2
assert-neq! "hello" "world"
assert-neq! [1, 2] [1, 2, 3]
assert-true!
Assert that a condition is true:
assert-true! (5 > 3)
assert-true! (String.contains? "hello" "ell")
assert-true! (List.empty? [])
assert-false!
Assert that a condition is false:
assert-false! (3 > 5)
assert-false! (String.contains? "hello" "xyz")
assert-false! (List.empty? [1, 2, 3])
assert-some!
Assert that an Option contains a value:
assert-some! (Some 42)
assert-some! (List.head [1, 2, 3])
assert-some! (Map.get "key" {key: "value"})
assert-none!
Assert that an Option is None:
assert-none! None
assert-none! (List.head [])
assert-none! (Map.get "missing" {})
assert-ok!
Assert that a Result is Ok:
assert-ok! (Ok 42)
assert-ok! (parse-int "123")
assert-err!
Assert that a Result is Err:
assert-err! (Err "failed")
assert-err! (parse-int "not a number")
Running Tests
Run tests using the interpreter:
# Run a single file with tests
kit run tests/math.test.kit
# Run your main file (if it contains tests)
kit run src/main.kit
Test Output
When tests run, you'll see output indicating success or failure:
# Successful tests
Running tests...
✓ addition works
✓ math operations
✓ list operations
3 tests passed
# Failed test
Running tests...
✓ addition works
✗ math operations
Assertion failed: expected 5, got 4
at line 8 in math.test.kit
1 of 2 tests failed
Organizing Tests
Inline Tests
For simple modules, you can include tests in the same file:
# math.kit
square = fn(x) => x * x
cube = fn(x) => x * x * x
factorial = fn(n) =>
if n <= 1 then 1
else n * factorial (n - 1)
# Tests
test "square" =>
assert-eq! (square 0) 0
assert-eq! (square 2) 4
assert-eq! (square 5) 25
test "cube" =>
assert-eq! (cube 2) 8
assert-eq! (cube 3) 27
test "factorial" =>
assert-eq! (factorial 0) 1
assert-eq! (factorial 1) 1
assert-eq! (factorial 5) 120
Separate Test Files
For larger projects, keep tests in separate files:
# tests/user.test.kit
import "../src/user.kit"
test "create user" =>
u = user.create 1 "Alice" "alice@example.com"
assert-eq! u.id 1
assert-eq! u.name "Alice"
assert-eq! u.email "alice@example.com"
test "validate user with valid email" =>
u = user.create 1 "Alice" "alice@example.com"
assert-ok! (user.validate u)
test "validate user with invalid email" =>
u = user.create 1 "Alice" "not-an-email"
assert-err! (user.validate u)
Project Structure
my-project/
kit.toml
src/
main.kit
user.kit
order.kit
tests/
user.test.kit
order.test.kit
integration.test.kit
Testing Patterns
Testing Pure Functions
test "map transforms list" =>
input = [1, 2, 3]
expected = [2, 4, 6]
result = map (fn(x) => x * 2) input
assert-eq! result expected
test "filter keeps matching elements" =>
input = [1, 2, 3, 4, 5, 6]
expected = [2, 4, 6]
result = filter (fn(x) => x % 2 == 0) input
assert-eq! result expected
Testing Error Cases
safe-divide = fn(a, b) =>
if b == 0 then
Err "Division by zero"
else
Ok (a / b)
test "safe-divide with valid inputs" =>
assert-eq! (safe-divide 10 2) (Ok 5)
assert-eq! (safe-divide 15 3) (Ok 5)
test "safe-divide with zero" =>
result = safe-divide 10 0
assert-err! result
match result
| Err msg -> assert-eq! msg "Division by zero"
| Ok _ -> assert-false! true # Should not reach here
Testing Option Returns
test "find returns Some when found" =>
items = [1, 2, 3, 4, 5]
result = List.find (fn(x) => x > 3) items
assert-some! result
assert-eq! result (Some 4)
test "find returns None when not found" =>
items = [1, 2, 3]
result = List.find (fn(x) => x > 10) items
assert-none! result
Testing Records
test "record creation and access" =>
point = {x: 10, y: 20}
assert-eq! point.x 10
assert-eq! point.y 20
test "record update" =>
point = {x: 10, y: 20}
moved = {x: point.x + 5, y: point.y + 5}
assert-eq! moved {x: 15, y: 25}
Testing Pattern Matching
type Shape =
| Circle Float
| Rectangle Float Float
| Square Float
area = fn(shape) =>
match shape
| Circle r -> 3.14159 * r * r
| Rectangle w h -> w * h
| Square s -> s * s
test "area of circle" =>
a = area (Circle 2.0)
assert-true! (a > 12.56 && a < 12.57)
test "area of rectangle" =>
assert-eq! (area (Rectangle 4.0 5.0)) 20.0
test "area of square" =>
assert-eq! (area (Square 3.0)) 9.0
Testing with Setup
# Helper function for test setup
make-test-users = fn() =>
[
{id: 1, name: "Alice", active?: true},
{id: 2, name: "Bob", active?: false},
{id: 3, name: "Carol", active?: true}
]
test "filter active users" =>
users = make-test-users
active = filter (fn(u) => u.active?) users
assert-eq! (length active) 2
test "find user by id" =>
users = make-test-users
result = List.find (fn(u) => u.id == 2) users
assert-some! result
match result
| Some u -> assert-eq! u.name "Bob"
| None -> assert-false! true
Best Practices
Write Focused Tests
# Good: One concept per test
test "map preserves list length" =>
input = [1, 2, 3, 4, 5]
result = map (fn(x) => x * 2) input
assert-eq! (length result) (length input)
test "map applies function to each element" =>
result = map (fn(x) => x * 2) [1, 2, 3]
assert-eq! result [2, 4, 6]
# Bad: Too many concepts in one test
test "map does everything" =>
input = [1, 2, 3]
result = map (fn(x) => x * 2) input
assert-eq! (length result) 3
assert-eq! result [2, 4, 6]
assert-eq! (head result) 2
assert-eq! (last result) 6
Test Edge Cases
test "map on empty list" =>
assert-eq! (map (fn(x) => x * 2) []) []
test "map on single element" =>
assert-eq! (map (fn(x) => x * 2) [5]) [10]
test "factorial of zero" =>
assert-eq! (factorial 0) 1
test "factorial of one" =>
assert-eq! (factorial 1) 1
Use Descriptive Names
# Good: Describes the scenario and expected behavior
test "parse-int returns Err for non-numeric string" =>
assert-err! (parse-int "hello")
test "parse-int returns Ok for valid integer" =>
assert-eq! (parse-int "42") (Ok 42)
# Bad: Vague names
test "test 1" =>
assert-err! (parse-int "hello")
test "parse-int works" =>
assert-eq! (parse-int "42") (Ok 42)
Keep Tests Independent
# Good: Each test creates its own data
test "filter removes items" =>
items = [1, 2, 3, 4, 5]
result = filter (fn(x) => x > 3) items
assert-eq! result [4, 5]
test "filter on empty list" =>
items = []
result = filter (fn(x) => x > 3) items
assert-eq! result []
Property-Based Testing
For more advanced testing, Kit provides the kit-quickcheck package for property-based testing. Instead of writing individual test cases, you describe properties that should hold for all inputs, and QuickCheck generates random test cases automatically.
import "kit-quickcheck"
# Property: reversing a list twice gives the original
test "reverse is self-inverse" =>
QuickCheck.for-all gen-list (fn(xs) =>
reverse (reverse xs) == xs
)
# Property: length is preserved by map
test "map preserves length" =>
QuickCheck.for-all gen-list (fn(xs) =>
length (map (fn(x) => x * 2) xs) == length xs
)
# Property: sorting is idempotent
test "sort is idempotent" =>
QuickCheck.for-all gen-list (fn(xs) =>
sort xs == sort (sort xs)
)
Property-based testing is particularly effective for finding edge cases and unexpected bugs that might not be caught by hand-written tests. See the kit-quickcheck documentation for more details.
Next Steps
Now that you understand testing, explore:
- Modules - Organize code and tests
- Standard Library - Built-in functions to test
- Error Handling - Testing Result and Option types
- kit-quickcheck - Property-based testing