Testing
Note: This feature is planned but not yet implemented. This page describes the intended design.
Keel will include a built-in testing framework.
Writing Tests
Tests are functions marked with #[test]:
import Test (expect, expectEqual)
#[test]
fn test_addition: Test
fn test_addition =
expectEqual 4 (2 + 2)
#[test]
fn test_string_concat: Test
fn test_string_concat =
expectEqual "hello world" ("hello " ++ "world")
Test Organization
Place tests in tests/ directory or alongside source with _test.kl suffix:
src/
├── math.kl
├── math_test.kl # Tests for math.kl
tests/
├── integration.kl # Integration tests
└── e2e.kl # End-to-end tests
Running Tests
keel test # Run all tests
keel test tests/unit.kl # Run specific file
keel test --filter "parse" # Run matching tests
keel test --verbose # Detailed output
Assertions
expectEqual
expectEqual expected actual
Assert two values are equal.
expectNotEqual
expectNotEqual a b
Assert values differ.
expectTrue / expectFalse
expectTrue condition
expectFalse condition
expectSome / expectNone
expectSome (Some 42)
expectNone None
expectOk / expectErr
expectOk (Ok 42)
expectErr (Err "failed")
expectThrows
expectThrows (\_ -> error "boom")
expectApprox
For floating-point comparison:
expectApprox 0.001 3.14159 pi
Test Setup
Before/After Hooks
#[before_each]
fn setup: IO ()
fn setup = do
initTestDatabase
#[after_each]
fn teardown: IO ()
fn teardown = do
cleanupTestDatabase
Before/After All
#[before_all]
fn setup_suite: IO ()
#[after_all]
fn teardown_suite: IO ()
Test Groups
Organize related tests:
#[test_group]
module ParserTests where
#[test]
fn test_parse_int: Test
fn test_parse_int = ...
#[test]
fn test_parse_string: Test
fn test_parse_string = ...
Skipping Tests
#[test, skip]
fn test_not_ready: Test
#[test, skip = "Waiting for feature X"]
fn test_pending: Test
Focused Tests
Run only specific tests:
#[test, only]
fn test_debugging: Test
Use --no-only to fail if only tests exist (CI safety).
Property-Based Testing
Generate random test cases:
import Test.Property (forAll, int, string)
#[test]
fn test_reverse_reverse: Test
fn test_reverse_reverse =
forAll (list int) (\xs ->
expectEqual xs (reverse (reverse xs))
)
#[test]
fn test_concat_length: Test
fn test_concat_length =
forAll (string, string) (\(a, b) ->
expectEqual
(String.length a + String.length b)
(String.length (a ++ b))
)
Mocking
import Test.Mock (mock, when, verify)
#[test]
fn test_with_mock: Test
fn test_with_mock = do
let mockDb = mock Database
when (mockDb.get "key") thenReturn (Some "value")
let result = myFunction mockDb
verify (mockDb.get "key") calledOnce
expectEqual expected result
Async Tests
#[test, async]
fn test_async_operation: IO Test
fn test_async_operation = do
result <- fetchData "url"
pure (expectOk result)
Timeout
#[test, timeout = 5000] -- 5 seconds
fn test_slow_operation: Test
Coverage
keel test --coverage
Generates coverage report in target/coverage/.
Continuous Integration
Example GitHub Actions:
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: keel-lang/setup-keel@v1
- run: keel test --coverage
Best Practices
- Test one thing per test — focused, descriptive names
- Use property tests for invariants
- Mock external dependencies — databases, APIs
- Run tests in CI — catch regressions early
- Aim for good coverage — but don't obsess over 100%