Test Runner
Tova has a built-in test runner that discovers and executes test blocks in .tova files. Tests are compiled to JavaScript and run via Bun's test infrastructure.
Running Tests
tova test # Run tests in the current directory
tova test src # Run tests in the src/ directory
tova test --filter "math" # Run only tests matching "math"
tova test --watch # Watch for changes and re-run
tova test --coverage # Enable coverage reporting
tova test --serial # Force sequential executionWriting Tests
Tests are defined using test blocks with a description string and a body:
test "addition works" {
assert_eq(1 + 1, 2)
}
test "string concatenation" {
result = "Hello" + " " + "World"
assert_eq(result, "Hello World")
}Test Timeout
Set a timeout (in milliseconds) for individual tests using the timeout option:
test "slow operation completes" timeout=5000 {
result = await long_running_task()
assert(result.isOk())
}If the test does not complete within the timeout, it fails with a timeout error.
Assertions
Tova provides three assertion functions for tests:
assert(condition)
Asserts that a condition is truthy:
test "basic assertions" {
assert(true)
assert(1 > 0)
assert(len([1, 2, 3]) == 3)
}assert_eq(actual, expected)
Asserts that two values are equal:
test "equality checks" {
assert_eq(2 + 2, 4)
assert_eq("hello", "hello")
assert_eq([1, 2], [1, 2])
}assert_ne(actual, expected)
Asserts that two values are not equal:
test "inequality checks" {
assert_ne(1, 2)
assert_ne("hello", "world")
assert_ne([], [1])
}Testing Functions
Define functions in the same file and test them:
fn factorial(n) {
match n {
0 => 1
n => n * factorial(n - 1)
}
}
test "factorial of 0" {
assert_eq(factorial(0), 1)
}
test "factorial of 5" {
assert_eq(factorial(5), 120)
}
test "factorial of 10" {
assert_eq(factorial(10), 3628800)
}Testing with Types
type Point {
x: Int
y: Int
}
fn distance(p1, p2) {
dx = p2.x - p1.x
dy = p2.y - p1.y
Math.sqrt(dx * dx + dy * dy)
}
test "distance between points" {
p1 = Point(0, 0)
p2 = Point(3, 4)
assert_eq(distance(p1, p2), 5)
}Testing Result and Option
fn safe_divide(a, b) {
match b {
0 => Err("division by zero")
_ => Ok(a / b)
}
}
test "safe division succeeds" {
result = safe_divide(10, 2)
assert_eq(result, Ok(5))
}
test "safe division by zero returns error" {
result = safe_divide(10, 0)
assert_eq(result, Err("division by zero"))
}Filtering Tests
Use --filter to run a subset of tests matching a pattern:
tova test --filter "factorial"This runs only tests whose description contains "factorial".
Watch Mode
Use --watch to automatically re-run tests when .tova files change:
tova test --watchThe watcher monitors the target directory recursively and re-runs the full test suite whenever a .tova file is modified.
Coverage
Use --coverage to enable Bun's built-in coverage reporting:
tova test --coverageThis shows which lines of your compiled code were exercised by the test suite.
Sequential Execution
By default, Bun runs test files in parallel. Use --serial to force sequential execution:
tova test --serialThis is useful when tests share global state (e.g., a database) and cannot run concurrently.
Test Organization
A recommended approach for organizing tests:
- Inline tests: Place
testblocks alongside the functions they test in the same.tovafile. This keeps tests close to the code they verify. - Dedicated test files: For larger test suites, create separate
.tovafiles (e.g.,math_test.tova,api_test.tova) in atests/directory.
my-project/
src/
app.tova # Application code with inline tests
utils.tova # Utility functions with inline tests
tests/
integration.tova # Integration testsSetup and Teardown
Use before_each and after_each blocks to run code before and after every test in the file:
var db = nil
before_each {
db = create_test_db()
}
after_each {
db.close()
}
test "can insert user" {
db.insert("users", { name: "Alice" })
assert_eq(db.count("users"), 1)
}
test "can query users" {
db.insert("users", { name: "Bob" })
users = db.query("SELECT * FROM users")
assert(len(users) > 0)
}Benchmarks
Use bench blocks to measure execution time of code snippets:
bench "array sorting" {
data = range(1000) |> shuffle()
sorted(data)
}
bench "string concatenation" {
var result = ""
for i in range(1000) {
result = result + "{i}"
}
}Run benchmarks with the dedicated bench command:
tova bench
tova bench srcProperty-Based Testing
Tova includes built-in support for property-based testing with random data generators and the forAll function.
Generators
Gen.int() // random integer
Gen.int(0, 100) // random integer in range
Gen.float() // random float
Gen.bool() // random boolean
Gen.string() // random string
Gen.string(10) // random string up to length 10
Gen.array(Gen.int()) // random array of integers
Gen.array(Gen.int(), 5) // random array up to length 5
Gen.oneOf(["a", "b", "c"]) // random choice from listforAll
test "reverse is its own inverse" {
forAll([Gen.array(Gen.int())], fn(arr) {
assert_eq(reversed(reversed(arr)), arr)
})
}
test "sort is idempotent" {
forAll([Gen.array(Gen.int())], fn(arr) {
assert_eq(sorted(sorted(arr)), sorted(arr))
})
}
// Configure number of runs
test "addition is commutative" {
forAll([Gen.int(), Gen.int()], fn(a, b) {
assert_eq(a + b, b + a)
}, { runs: 500 })
}Snapshot Testing
Use assert_snapshot to compare a value against a stored snapshot. On the first run, the snapshot is created. On subsequent runs, the value is compared against the saved snapshot:
test "user serialization" {
user = User(1, "Alice", "alice@example.com")
assert_snapshot(user.to_json())
}
test "html rendering" {
html = render_component(Greeting("World"))
assert_snapshot(html, "greeting-output")
}Spy and Mock
Spies
Create a spy to track function calls:
test "callback is called" {
spy = create_spy()
run_with_callback(spy)
assert(spy.called)
assert_eq(spy.call_count, 1)
}
test "callback receives correct args" {
spy = create_spy()
process(items, on_complete: spy)
assert(spy.called_with([items]))
}
test "spy with implementation" {
spy = create_spy(fn(x) x * 2)
result = spy(5)
assert_eq(result, 10)
assert_eq(spy.call_count, 1)
}Spy properties:
.called--trueif called at least once.call_count-- number of times called.calls-- array of argument arrays from each call.called_with(args)-- returnstrueif called with matching args.reset()-- clears all call tracking
Mocks
Create a mock that returns a fixed value:
test "uses mock data" {
mock_fetch = create_mock({ status: 200, body: "ok" })
result = process_response(mock_fetch)
assert_eq(result, "ok")
assert(mock_fetch.called)
}assert_throws
Assert that a function throws an error:
test "division by zero throws" {
assert_throws(fn() divide(1, 0))
}
test "validation rejects bad input" {
assert_throws(fn() validate_email("not-an-email"), "invalid email")
}See the Assertions stdlib page for full details on assert_throws.
How It Works
Under the hood, tova test performs these steps:
- Scans the target directory for
.tovafiles containingtestblocks - Compiles each file to JavaScript, extracting the test code
- Writes compiled test files to
.tova-test-out/ - Runs the compiled tests using
bun test - Reports results and exits with the appropriate exit code