Testing: test blocks
● Intermediate3 min
A test block checks a story of several steps that contracts can't express.
type Order {
id: Int
paid: Bool
}
fn total_paid(orders: [Order]) -> Int {
let n = 0
for o in orders {
if o.paid {
n += 1
}
}
n
}
test "only paid orders count" {
const list = [Order(id: 1, paid: true), Order(id: 2, paid: false), Order(id: 3, paid: true)]
assert total_paid(list) == 2
}The program ran and printed:
kavach run story.kav
✓ test "only paid orders count"1 passed, 0 failedseed 7✓ total_paid 200 cases1 functions 1 ✓ 0 ✗ 1 weakly testedweakly tested (no contract or example; only checked for panics): story.kav:6 total_paid
assert checks a condition. The report also flags total_paid as weakly tested: it has a story but no contract, so only some of its behaviour is checked.
use std.random
fn roll() -> Int uses Random
ensure 1 <= result <= 6
{
random.int(1, 7)
}
test "rolls stay on the die" {
for _ in 0..100 {
assert 1 <= roll() <= 6
}
}The program ran and printed:
kavach run random.kav
✓ test "rolls stay on the die"1 passed, 0 failedseed 7✓ roll 1 cases1 functions 1 ✓ 0 ✗ 0 weakly tested
Tests use nothing unless given
A test runs with no capabilities unless its name is followed by uses. Reaching one without it, directly or through a function, is a compile error:
use std.fs
fn save(text: String) ! FsError uses Fs {
try fs.write_text("report.txt", text)
}
test "writes a report" {
try save("hi")
}The compiler refused the program:
kavach run test-uses.kav
error: this test uses Fs, but a test runs with no capabilities unless given some: write `uses Fs` after its name, or give it a fake with `with` (spec 11.8)Say what the test needs with uses. A test given Fs, Net, Env or Clock reaches the real one. Add with Fs = fake to get an empty file system in memory instead, never full, so nothing the test writes reaches the disk:
use std.fs
fn save(text: String) ! FsError uses Fs {
try fs.write_text("report.txt", text)
}
test "writes a report" uses Fs with Fs = fake {
try save("hi")
assert (try fs.read_text("report.txt")) == "hi"
}The program ran and printed:
kavach run test-uses.kav
✓ test "writes a report"1 passed, 0 failedseed 7✓ save 200 cases1 functions 1 ✓ 0 ✗ 1 weakly testedweakly tested (no contract or example; only checked for panics): test-uses.kav:3 save
- The console, the clock and random numbers are always there: a test run captures the output, can freeze the clock and seeds the numbers.
- A test can also hand in its own implementation of a capability:
test "refund flow" with Payments = FakePayments() { … }.
with Net = fake, with Env = fake and with Clock = fake give the other fakes the same way.
A test ends with its tasks
A test block is a function of its own for its tasks: those it spawned and left running are cancelled when it ends, as a function's are (Tasks Never Leak). Here the worker still waits for a job when the test ends; it is cancelled, and the run goes on. A server a test started doesn't keep the run going either.
/// Waits for jobs, one at a time, until the channel closes.
fn worker(jobs: Receiver<Int>, done: Sender<Int>) {
for job in jobs {
done.put(job * 10)
}
}
test "a worker left waiting ends with the test" {
const (send, jobs) = Channel<Int>(capacity: 1)
const (done, results) = Channel<Int>(capacity: 1)
const _worker = spawn worker(jobs, done)
send.put(4)
assert results.take() == Taken.Item(value: 40)
}The program ran and printed:
kavach run left-task.kav
✓ test "a worker left waiting ends with the test"1 passed, 0 failedseed 70 functions 0 ✓ 0 ✗ 0 weakly tested not generated: worker: takes values that can't be generated yet
A test, an example or a generated case that runs past sixty times time_budget (a minute by default) is stopped. kavach test names it and fails, so one waiting for something that never comes can't hang the run:
error: test "…" has run for N s, past the limit of M s, and was stopped (spec 11.9)Where test blocks go
Beside the code they test: the tests of orders.kav go in orders.test.kav, in the same folder. A glance at a folder shows which modules have hand-written tests.
pub type Order {
pub amount: Int
pub paid: Bool
}
/// What the paid orders add up to.
pub fn total_paid(orders: [Order]) -> Int
require orders.len() <= 1_000
require orders.all(o => o.amount >= 0 and o.amount <= 1_000_000)
example total_paid([]) == 0
{
orders.filter(o => o.paid).sum(o => fee(o.amount))
}
fn fee(amount: Int) -> Int
require amount >= 0 and amount <= 1_000_000
example fee(10) == 11
{
amount + 1
}test "only paid orders count" {
const list = [Order(amount: 100, paid: true), Order(amount: 50, paid: false)]
assert total_paid(list) == 101
}
test "the fee is one" {
assert fee(10) == 11
}use orders.{Order, total_paid}
fn main() {
print(total_paid([Order(amount: 20, paid: true), Order(amount: 5, paid: true)]))
}27
✓ total_paid: example total_paid([]) == 0 ✓ fee: example fee(10) == 11 ✓ test "only paid orders count" ✓ test "the fee is one" 4 passed, 0 failed seed 7 ✓ total_paid 200 cases ✓ fee 200 cases 2 functions 2 ✓ 0 ✗ 0 weakly tested
orders.test.kavis part of the moduleorders: it uses the privatefee, with nouse.kavach runleaves the tests out;kavach testruns them with the examples and the generated tests.- A story across many modules goes in the project's
tests/folder instead, and a test block may also sit in the file it tests. - A test file must be named after a module beside it. Otherwise it is an error that says so:
test "an invoice adds up" {
assert 1 + 1 == 2
}fn main() {
print("hi")
}The compiler refused the program:
kavach run
error: `invoice.test.kav` holds the tests of `invoice.kav`, but there is no `invoice.kav` beside it: name it after the module it tests (spec 11.8)