Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Testing

This chapter covers testing guidelines, patterns, and best practices for contributing to LatticeDB.

Test Organization

Test Location

Test TypeLocationCommand
Unit testssrc/*.rs (inline #[cfg(test)])cargo test
Integration teststests/*.rscargo test --test <name>
WASM testssrc/*.rs with wasm_bindgen_testwasm-pack test
Benchmarksbenches/*.rscargo bench

Module Structure

#![allow(unused)]
fn main() {
// src/hnsw.rs

pub struct HnswIndex { ... }

impl HnswIndex {
    pub fn search(&self, query: &[f32], k: usize, ef: usize) -> Vec<SearchResult> {
        // Implementation
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_search_returns_k_results() {
        // Test implementation
    }
}
}

Writing Unit Tests

Basic Test Pattern

#![allow(unused)]
fn main() {
#[test]
fn test_function_name_describes_behavior() {
    // Arrange
    let index = HnswIndex::new(test_config(), Distance::Cosine);
    let point = Point::new_vector(1, vec![0.1, 0.2, 0.3]);

    // Act
    index.insert(&point);
    let results = index.search(&[0.1, 0.2, 0.3], 10, 100);

    // Assert
    assert_eq!(results.len(), 1);
    assert_eq!(results[0].id, 1);
}
}

Test Naming Convention

#![allow(unused)]
fn main() {
// Good: Describes behavior
#[test]
fn test_search_returns_empty_for_empty_index() { ... }

#[test]
fn test_insert_updates_existing_point_with_same_id() { ... }

#[test]
fn test_delete_returns_false_for_nonexistent_point() { ... }

// Bad: Vague names
#[test]
fn test_search() { ... }

#[test]
fn test_insert() { ... }
}

Test Helpers

#![allow(unused)]
fn main() {
// Common test configuration
fn test_config() -> HnswConfig {
    HnswConfig {
        m: 16,
        m0: 32,
        ml: HnswConfig::recommended_ml(16),
        ef: 100,
        ef_construction: 200,
    }
}

// Random vector generation (deterministic)
fn random_vector(dim: usize, seed: u64) -> Vector {
    let mut rng = seed;
    (0..dim)
        .map(|_| {
            rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1);
            (rng as f64 / u64::MAX as f64) as f32
        })
        .collect()
}

// Approximate equality for floats
fn approx_eq(a: f32, b: f32, epsilon: f32) -> bool {
    (a - b).abs() < epsilon
}
}

WASM Tests

Configuration

#![allow(unused)]
fn main() {
// At the top of the test module
#[cfg(all(target_arch = "wasm32", test))]
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
}

Test Attribute

#![allow(unused)]
fn main() {
#[cfg(target_arch = "wasm32")]
use wasm_bindgen_test::wasm_bindgen_test as test;

#[test]
fn test_works_on_both_native_and_wasm() {
    // This test runs on both platforms
}
}

Async WASM Tests

#![allow(unused)]
fn main() {
#[cfg(target_arch = "wasm32")]
use wasm_bindgen_test::wasm_bindgen_test;

#[wasm_bindgen_test]
async fn test_async_storage_operation() {
    let storage = OpfsStorage::new().await.unwrap();
    storage.write_page(0, b"hello").await.unwrap();
    let page = storage.read_page(0).await.unwrap();
    assert_eq!(page, b"hello");
}
}

Running WASM Tests

# Chrome (headless)
wasm-pack test --headless --chrome crates/lattice-core

# Firefox
wasm-pack test --headless --firefox crates/lattice-core

# With output
wasm-pack test --headless --chrome crates/lattice-core -- --nocapture

Testing Async Code

Basic Async Test

#![allow(unused)]
fn main() {
#[tokio::test]
async fn test_async_operation() {
    let storage = MemStorage::new();
    storage.write_page(0, b"test").await.unwrap();
    let data = storage.read_page(0).await.unwrap();
    assert_eq!(data, b"test");
}
}

Testing with Timeouts

#![allow(unused)]
fn main() {
#[tokio::test]
async fn test_operation_completes_quickly() {
    let result = tokio::time::timeout(
        Duration::from_secs(5),
        expensive_operation()
    ).await;

    assert!(result.is_ok(), "Operation timed out");
}
}

Property-Based Testing

Using proptest

#![allow(unused)]
fn main() {
use proptest::prelude::*;

proptest! {
    #[test]
    fn test_quantize_dequantize_preserves_order(
        a in prop::collection::vec(-1.0f32..1.0, 1..128),
        b in prop::collection::vec(-1.0f32..1.0, 1..128),
    ) {
        // Ensure same length
        let len = a.len().min(b.len());
        let a = &a[..len];
        let b = &b[..len];

        let qa = QuantizedVector::quantize(a);
        let qb = QuantizedVector::quantize(b);

        let original_dist = cosine_distance(a, b);
        let quantized_dist = qa.cosine_distance_asymmetric(b);

        // Quantization should preserve relative ordering
        // (within some error margin)
        prop_assert!((original_dist - quantized_dist).abs() < 0.2);
    }
}
}

Integration Tests

Test File Structure

#![allow(unused)]
fn main() {
// tests/integration_test.rs
use lattice_core::*;
use lattice_storage::MemStorage;

#[tokio::test]
async fn test_full_workflow() {
    // Create collection
    let config = CollectionConfig::new(
        "test_collection",
        VectorConfig::new(128, Distance::Cosine),
        HnswConfig::default(),
    );
    let storage = MemStorage::new();
    let mut engine = CollectionEngine::new(config, storage).unwrap();

    // Insert points
    for i in 0..100 {
        let point = Point::new_vector(i, random_vector(128, i));
        engine.upsert(point).unwrap();
    }

    // Search
    let query = random_vector(128, 999);
    let results = engine.search(&SearchQuery::new(query).with_limit(10)).unwrap();

    assert_eq!(results.len(), 10);
}
}

Benchmarks

Criterion Benchmark

#![allow(unused)]
fn main() {
// benches/search_bench.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion};

fn bench_search(c: &mut Criterion) {
    // Setup
    let index = create_test_index(10000);
    let query = random_vector(128, 0);

    c.bench_function("search_10k", |b| {
        b.iter(|| {
            black_box(index.search(&query, 10, 100))
        })
    });
}

criterion_group!(benches, bench_search);
criterion_main!(benches);
}

Running Benchmarks

# Run all benchmarks
cargo bench -p lattice-bench

# Run specific benchmark
cargo bench -p lattice-bench -- search

# Compare to baseline
cargo bench -p lattice-bench -- --baseline main

Testing Best Practices

1. Test Edge Cases

#![allow(unused)]
fn main() {
#[test]
fn test_search_empty_index() {
    let index = HnswIndex::new(config, Distance::Cosine);
    let results = index.search(&[0.1, 0.2], 10, 100);
    assert!(results.is_empty());
}

#[test]
fn test_search_k_greater_than_index_size() {
    let mut index = HnswIndex::new(config, Distance::Cosine);
    index.insert(&Point::new_vector(1, vec![0.1, 0.2]));

    let results = index.search(&[0.1, 0.2], 100, 100);
    assert_eq!(results.len(), 1);  // Returns available, not k
}

#[test]
fn test_quantize_zero_vector() {
    let quantized = QuantizedVector::quantize(&[0.0, 0.0, 0.0]);
    assert!(!quantized.is_empty());
}
}

2. Test Error Conditions

#![allow(unused)]
fn main() {
#[test]
fn test_delete_nonexistent_returns_false() {
    let mut index = HnswIndex::new(config, Distance::Cosine);
    assert!(!index.delete(999));
}

#[test]
fn test_storage_error_on_missing_page() {
    let storage = MemStorage::new();
    let result = storage.read_page(999).await;
    assert!(matches!(result, Err(StorageError::PageNotFound { .. })));
}
}

3. Use Descriptive Assertions

#![allow(unused)]
fn main() {
// Good: Clear failure message
assert_eq!(
    results.len(),
    10,
    "Expected 10 results but got {}, query: {:?}",
    results.len(),
    query
);

// Bad: Unhelpful failure
assert!(results.len() == 10);
}

4. Isolate Tests

#![allow(unused)]
fn main() {
// Good: Each test is independent
#[test]
fn test_insert_single() {
    let mut index = HnswIndex::new(config, Distance::Cosine);
    index.insert(&point);
    assert_eq!(index.len(), 1);
}

// Bad: Tests depend on shared state
static mut SHARED_INDEX: Option<HnswIndex> = None;

#[test]
fn test_1_insert() {
    unsafe { SHARED_INDEX = Some(HnswIndex::new(...)); }
}

#[test]
fn test_2_search() {
    // Fails if test_1 didn't run first!
}
}

5. Test Recall Statistically

#![allow(unused)]
fn main() {
#[test]
fn test_recall_at_least_90_percent() {
    let index = create_index_with_1000_points();
    let distance = DistanceCalculator::new(Distance::Cosine);

    let mut total_recall = 0.0;
    let num_queries = 100;

    for q in 0..num_queries {
        let query = random_vector(128, 10000 + q);

        // Ground truth via brute force
        let gt = brute_force_search(&index, &query, 10);

        // HNSW search
        let results = index.search(&query, 10, 100);

        // Calculate recall
        let hits = gt.iter()
            .filter(|&id| results.iter().any(|r| r.id == *id))
            .count();
        total_recall += hits as f64 / 10.0;
    }

    let avg_recall = total_recall / num_queries as f64;
    assert!(
        avg_recall >= 0.9,
        "Average recall {:.3} below 90% threshold",
        avg_recall
    );
}
}

Coverage

Generate Coverage Report

# Install grcov
cargo install grcov

# Run tests with coverage
CARGO_INCREMENTAL=0 RUSTFLAGS='-Cinstrument-coverage' \
    LLVM_PROFILE_FILE='cargo-test-%p-%m.profraw' \
    cargo test --workspace

# Generate HTML report
grcov . --binary-path ./target/debug/deps/ -s . -t html --branch --ignore-not-existing -o ./target/coverage/

# Open report
open target/coverage/index.html

Next Steps