Test-Driven Development with hica
20 May 2026hica has inline test blocks. They sit right next to your functions: no extra test files, no additional imports and no framework, sounds amazing right!
Run hica test and you’re done, that’s it!
Early on in hica’s design I wanted a super-easy way to run tests, like the easiest way possible and I succeeded (IMHO)
This post walks through how TDD works in hica and why the compiler changes what you need to test.
TDD isn’t new
TDD has a reputation as a methodology someone invented. It’s most definitely not. Kent Beck has said he “rediscovered” it by studying older literature and watching what good programmers already did. Dijkstra argued in 1972 that correctness proof and programs should “grow hand in hand.”
Andrea Laforgia captures this well in his T*D piece. TDD, trunk-based development, and collaborative programming are patterns that keep emerging when teams care about quality and fast delivery. When you remove friction from testing, people test first. That’s what happened with hica.
The compiler does half the work
hica has Hindley-Milner inference and exhaustive pattern matching. A lot of the defensive tests I’d write in other languages aren’t needed:
type Direction {
North, South, East, West
}
fun move(pos, dir) {
match dir {
North => (pos.0, pos.1 + 1),
South => (pos.0, pos.1 - 1),
East => (pos.0 + 1, pos.1),
West => (pos.0 - 1, pos.1)
}
}
Forget the null case (there’s no null). Pass a string where a Direction is expected? It won’t compile. Leave out West? Compile error.
The type checker handles structural correctness. What’s left for tests is whether the function does the right thing.
If the compiler can’t tell a correct implementation from an incorrect one, write a test <– rule of thumb!
Red-green-refactor
Let’s build a simple password validator from scratch.
Red. Stub the interface, assert what you want:
fun validate(password) => false
test "valid password has 6+ characters" {
assert(validate("secret123"))
}
hica test password.hc fails. Good, that’s expected.
Green. Minimum code to pass:
fun validate(password) => str_length(password) >= 6
Passes.
Red again. New requirement: passwords need at least one digit. Add a test for it, it will fail against the current validate:
test "rejects password without digit" {
assert_false(validate("noNumbersHere"))
}
Green. Update the implementation to pass all tests:
fun has_digit(s) => any(chars(s), (c) => is_digit(c))
fun validate(password) {
let long_enough = str_length(password) >= 6
long_enough && has_digit(password)
}
test "valid password has 6+ characters" {
assert(validate("secret123"))
}
test "rejects password without digit" {
assert_false(validate("noNumbersHere"))
}
test "rejects short password" {
assert_false(validate("ab1"))
}
All three pass. Refactor whenever, the tests catch regressions.
No context switch
Tests live next to the code:
fun double(x) => x * 2
test "double works" {
assert_eq(double(3), 6)
assert_eq(double(0), 0)
}
running 1 test(s)...
✓ double works
1 test(s) passed
When something breaks:
✗ double works
assertion failed: assert_eq(double(3), 5)
expected: 5
actually: 6
Use assert_eq over assert: “expected 5, got 6” tells you what happened, assert just says “false”.
A typical session
hica repl # explore an API before writing tests
nvim password.hc # write a failing test
hica test password.hc # see it fail
nvim password.hc # make it pass
hica test password.hc # see it pass
hica check password.hc # fast type-check between edits
tbdflow commit -t feat -m "add password validation"
hica check only type-checks, no compilation and near instant. I run it between every edit, like a spellchecker for logic 🙂
hica repl is useful when you’re not sure what a function returns. Try it interactively first, then write the test.
One test per behaviour
“Validates length” and “requires digit” are separate tests. If a test name needs “and” in it, split it.
Happy testing!
Throughput is a safety feature! (now with tests)