Testing¶
Spectre provides built-in support for unit testing through test blocks and assertions. This document covers the testing framework and best practices for writing tests.
Overview¶
Testing in Spectre is designed to verify that functions behave correctly according to their contracts. The testing framework includes:
Test blocks: Dedicated blocks for organizing tests
Assertions: Statements that verify expected behavior
Integration with contracts: Tests can verify contract satisfaction
Test Blocks¶
Test blocks are defined using the test keyword. They contain assertions that verify function behavior.
Basic Syntax¶
test {
assert condition
}
Test Block Location¶
Test blocks are typically placed after function definitions:
pub fn add(x: i32, y: i32) i32 = {
return x + y
}
test {
assert add(1, 2) == 3
}
Multiple Assertions¶
Test blocks can contain multiple assertions:
pub fn add(x: i32, y: i32) i32 = {
return x + y
}
test {
assert add(1, 2) == 3
assert add(0, 0) == 0
assert add(-1, 1) == 0
assert add(100, 200) == 300
}
Assertions¶
Assertions verify that a condition is true. If an assertion fails, the test fails.
Basic Assertions¶
assert expression == expected_value
Assertion Examples¶
// Equality assertions
assert add(1, 2) == 3
assert result == 0
assert value == 100
// Comparison assertions
assert x > 0
assert y <= 10
assert z != 5
// Boolean assertions
assert is_valid
assert has_completed
Testing Functions with Contracts¶
Tests can verify that functions satisfy their contracts.
Testing Pre-conditions¶
fn divide(a: i32, b: i32) i32 = {
pre {
not_zero : b != 0
}
val result = a / b
return result
}
test {
assert divide(10, 2) == 5
assert divide(100, 10) == 10
}
Testing Post-conditions¶
fn double(x: i32) i32 = {
pre {
x > 0
}
val result = x * 2
post {
result_is_double : result == x * 2
}
return result
}
test {
assert double(5) == 10
assert double(10) == 20
}
Testing Option Types¶
Functions that return option types can be tested for both some and none cases.
Testing Some Values¶
fn check(fail: bool) option[i32]! = {
if (fail) {
return some 10
}
return none
}
test {
// Test some case
assert check(true) == some 10
}
Testing None Values¶
test {
// Test none case
assert check(false) == none
}
Testing Trusted Functions¶
Functions marked with the trust marker (!) can be tested like regular functions.
pub fn some_other_function() void! = {
std.io.print("This function has no contracts")
}
test {
// Test that the function completes without error
some_other_function()
}
Testing Structs¶
Structs can be tested by verifying field values and mutability.
Testing Struct Creation¶
type SomeType = {
x: i32
y: mut i32
}
test {
val st: SomeType = {x: 30, y: 40}
// Verify field values
// Note: Direct field access syntax may vary by implementation
}
Testing Mutable Structs¶
type Point = {
x: mut i32
y: mut i32
}
test {
val p: mut Point = {x: 1, y: 2}
p.x = 10
p.y = 20
// Verify mutations
}
Complete Test Example¶
A complete example demonstrating various testing patterns:
val std = use("std")
pub fn add(x: i32, y: i32) i32 = {
return x + y
}
pub fn subtract(x: i32, y: i32) i32 = {
return x - y
}
fn multiply(x: i32, y: i32) i32 = {
pre {
x > 0 && y > 0
}
return x * y
}
test {
// Test public functions
assert add(1, 2) == 3
assert add(0, 0) == 0
assert add(-1, 1) == 0
// Test subtraction
assert subtract(5, 3) == 2
assert subtract(10, 10) == 0
// Test private function with contracts
assert multiply(2, 3) == 6
assert multiply(10, 10) == 100
}
Test Organization¶
Test Files¶
Tests can be placed in the same file as the code they test, or in separate test files:
math.spr:
pub fn add(x: i32, y: i32) i32 = {
return x + y
}
test {
assert add(1, 2) == 3
}
Best Practices¶
Writing Effective Tests¶
Test edge cases: Include tests for boundary conditions
Test both success and failure: For option types, test both
someandnoneUse descriptive test organization: Group related assertions together
Test contract satisfaction: Verify that functions meet their contracts
Test Coverage¶
Cover all public functions: Every public function should have tests
Test private functions: Private functions with contracts should be tested
Test error paths: Include tests for error conditions
Example: Comprehensive Test Suite¶
val std = use("std")
type Point = {
x: mut i32
y: mut i32
}
pub fn add(x: i32, y: i32) i32 = {
return x + y
}
fn multiply(x: i32, y: i32) i32 = {
pre {
x > 0 && y > 0
}
return x * y
}
fn check(fail: bool) option[i32]! = {
if (fail) {
return some 10
}
return none
}
// Test basic arithmetic
test {
assert add(1, 2) == 3
assert add(0, 0) == 0
assert add(-1, 1) == 0
assert add(100, 200) == 300
}
// Test multiplication with contracts
test {
assert multiply(2, 3) == 6
assert multiply(10, 10) == 100
assert multiply(1, 1) == 1
}
// Test option types
test {
assert check(true) == some 10
assert check(false) == none
}
Running Tests¶
The method for running tests depends on the Spectre toolchain. Typically, tests are run using a command such as:
spectre test <file.spr>
Or for a full project:
spectre test
Test Output¶
When tests pass, the test runner typically reports success. When tests fail, the output includes:
The failing assertion
The expected value
The actual value
The location of the failure
Integration with Contracts¶
Tests and contracts work together to ensure correctness:
Contracts define behavior: Pre-conditions and post-conditions specify what functions do
Tests verify behavior: Tests confirm that functions behave as specified
Contracts catch runtime errors: Contract violations cause panics
Tests catch logic errors: Failed assertions indicate incorrect logic
Summary¶
Spectre’s testing framework provides:
Test blocks:
test { assert condition }Assertions:
assert expressionContract testing: Verify contract satisfaction
Option type testing: Test
someandnonecasesTrusted function testing: Test functions with
!marker
Key points:
Use
testblocks to organize testsUse
assertto verify conditionsTest all public functions
Include edge cases and error conditions
Verify contract satisfaction through tests
For more information on error handling in tests, see the Error Handling documentation.