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

  1. Test edge cases: Include tests for boundary conditions

  2. Test both success and failure: For option types, test both some and none

  3. Use descriptive test organization: Group related assertions together

  4. Test contract satisfaction: Verify that functions meet their contracts

Test Coverage

  1. Cover all public functions: Every public function should have tests

  2. Test private functions: Private functions with contracts should be tested

  3. 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:

  1. Contracts define behavior: Pre-conditions and post-conditions specify what functions do

  2. Tests verify behavior: Tests confirm that functions behave as specified

  3. Contracts catch runtime errors: Contract violations cause panics

  4. Tests catch logic errors: Failed assertions indicate incorrect logic

Summary

Spectre’s testing framework provides:

  • Test blocks: test { assert condition }

  • Assertions: assert expression

  • Contract testing: Verify contract satisfaction

  • Option type testing: Test some and none cases

  • Trusted function testing: Test functions with ! marker

Key points:

  1. Use test blocks to organize tests

  2. Use assert to verify conditions

  3. Test all public functions

  4. Include edge cases and error conditions

  5. Verify contract satisfaction through tests

For more information on error handling in tests, see the Error Handling documentation.