Esc
Start typing to search...

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

  1. Test one thing per test — focused, descriptive names
  2. Use property tests for invariants
  3. Mock external dependencies — databases, APIs
  4. Run tests in CI — catch regressions early
  5. Aim for good coverage — but don't obsess over 100%