Testing

Write and run tests for Haskell and Python code. Use when adding tests, verifying behavior, or debugging test failures.

Required Companion Skills

Before writing implementation for tested behavior, also load:

Process

  1. Identify the relevant namespace and the test entrypoint.
  2. Run bild --test <namespace> to build and execute tests.
  3. If tests fail, isolate a single case and reproduce locally.
  4. Fix the code or tests, then re-run the suite.
  5. Add new tests for regressions before finishing.

Examples

bild --test Omni/Task.hs
_/bin/task test

Running Tests

bild --test Omni/Task.hs    # Build and run tests
_/bin/task test             # Run tests on existing binary

Test Convention

All programs follow the pattern: if first arg is test, run tests.

main :: IO ()
main = do
  args <- getArgs
  case args of
    ["test"] -> runTests
    _ -> normalMain

Writing Tests (Haskell)

Use HUnit:

-- : dep HUnit

import Test.HUnit

runTests :: IO ()
runTests = do
  results <- runTestTT $ TestList
    [ "parse valid input" ~: parseInput "foo" ~?= Right Foo
    , "parse empty fails" ~: parseInput "" ~?= Left "empty input"
    , "handles unicode" ~: parseInput "日本語" ~?= Right (Text "日本語")
    ]
  when (failures results > 0) exitFailure

Test helpers

-- Test that something throws
assertThrows :: IO a -> IO ()
assertThrows action = do
  result <- try action
  case result of
    Left (_ :: SomeException) -> pure ()
    Right _ -> assertFailure "Expected exception"

-- Test with setup/teardown
withTempFile :: (FilePath -> IO a) -> IO a
withTempFile action = do
  path <- emptySystemTempFile "test"
  action path `finally` removeFile path

Writing Tests (Python)

Use pytest:

# : dep pytest

import pytest

def test_parse_valid():
    assert parse_input("foo") == Foo()

def test_parse_empty_fails():
    with pytest.raises(ValueError):
        parse_input("")

def test_handles_unicode():
    assert parse_input("日本語") == Text("日本語")

Fixtures

@pytest.fixture
def temp_db():
    db = create_temp_database()
    yield db
    db.cleanup()

def test_with_db(temp_db):
    temp_db.insert({"key": "value"})
    assert temp_db.get("key") == "value"

What to Test

Do test

Don’t over-test

Test-Driven Debugging

When something breaks:

  1. Write a test that reproduces the bug
  2. Verify test fails
  3. Fix the code
  4. Verify test passes
  5. Keep the test (prevents regression)

Test Organization

runTests :: IO ()
runTests = do
  results <- runTestTT $ TestList
    [ -- Group related tests
      TestLabel "Parsing" $ TestList
        [ "valid input" ~: ...
        , "invalid input" ~: ...
        ]
    , TestLabel "Serialization" $ TestList
        [ "round trip" ~: ...
        ]
    ]

Common Test Patterns

Property-based (Haskell)

-- : dep QuickCheck

import Test.QuickCheck

prop_roundTrip :: Text -> Bool
prop_roundTrip t = decode (encode t) == Just t

runTests = quickCheck prop_roundTrip

Parameterized (Python)

@pytest.mark.parametrize("input,expected", [
    ("foo", Foo()),
    ("bar", Bar()),
    ("", None),
])
def test_parse(input, expected):
    assert parse(input) == expected

Mocking

from unittest.mock import patch

def test_api_call():
    with patch('module.requests.get') as mock_get:
        mock_get.return_value.json.return_value = {"key": "value"}
        result = fetch_data()
        assert result == {"key": "value"}

Debugging Test Failures

  1. Run single test: pytest test_file.py::test_name -v
  2. Add print statements
  3. Check test isolation (does it pass alone but fail with others?)
  4. Check for shared mutable state
  5. Check for timing/race conditions