githubEdit

Testing

This reference covers testing strategies for Substreams applications, from unit testing individual handlers to integration testing complete modules with real blockchain data.

Why Testing Matters

Blockchain data is immutable and mistakes are permanent. Testing is critical because:

  • Small bugs amplify across millions of blocks

  • Complex transformations introduce edge cases

  • DeFi applications depend on data accuracy

  • Parallel execution can create race conditions

Unit Testing

The substreams::testing module provides utilities for testing map handlers directly without WASM compilation overhead.

The map! Macro

The substreams::testing::map! macro allows you to invoke map handlers directly in unit tests:

use substreams::errors::Error;
use substreams_ethereum::pb::eth::v2::Block;

mod pb;
use pb::myproject::{Events, Event};

#[substreams::handlers::map]
pub fn map_events(block: Block) -> Result<Events, Error> {
    let events: Vec<Event> = block.logs()
        .filter(|log| !log.topics.is_empty())
        .filter_map(|log| parse_event(log).ok())
        .collect();

    Ok(Events { events })
}

#[substreams::handlers::map]
pub fn filter_events(
    event_type: String,
    events: Events,
) -> Result<Events, Error> {
    let filtered: Vec<Event> = events.events
        .into_iter()
        .filter(|e| e.event_type == event_type)
        .collect();

    Ok(Events { events: filtered })
}

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

    #[test]
    fn test_map_events() {
        // Arrange
        let block = create_test_block();

        // Act
        let result = testing::map!(map_events(block)).unwrap();

        // Assert
        assert!(!result.events.is_empty());
    }

    #[test]
    fn test_map_events_empty_block() {
        let empty_block = Block::default();

        let result = testing::map!(map_events(empty_block)).unwrap();

        assert!(result.events.is_empty());
    }

    #[test]
    fn test_chained_handlers() {
        let block = create_test_block();

        // Chain multiple handlers together
        let all_events = testing::map!(map_events(block)).unwrap();
        let transfers = testing::map!(filter_events("transfer".to_string(), all_events)).unwrap();

        assert!(transfers.events.iter().all(|e| e.event_type == "transfer"));
    }

    fn create_test_block() -> Block {
        // Create a test block with sample data
        Block {
            number: 17000000,
            // ... populate with test data
            ..Default::default()
        }
    }
}

The clock() Function

For modules that depend on block time or need clock information:

Opting Out of Testable Generation

Map handlers automatically generate testable functions that the map! macro uses. In rare cases where you don't want this behavior:

Test Data

Using Real Blockchain Data

Use the firecorearrow-up-right tool to extract real blocks for testing:

Load the block in your tests:

Constructing Test Blocks

For controlled testing scenarios, construct blocks programmatically:

Test Data Builder Pattern

For complex test scenarios, use a builder pattern:

Integration Testing

Test complete modules with real blockchain data to verify behavior across multiple blocks.

End-to-End Testing

Execute complete Substreams in the real execution environment using the CLI:

Wrap CLI commands in Rust tests for automated validation:

Performance Testing

Benchmarking with Criterion

Use the Criterionarrow-up-right crate for reliable benchmarks:

Production Mode Validation

Test with production mode to validate caching behavior and performance:

Testing Best Practices

Test Structure

Follow the Given-When-Expect pattern for clarity:

Error Scenario Testing

Test malformed data and edge cases:

Reorganization Testing

For stores that maintain state, test chain reorganization scenarios:

CI/CD Integration

GitHub Actions Example

Pre-commit Hooks

Automate quality checks before commits:

Summary

Test Level
Purpose
Tools

Unit

Test individual handlers

substreams::testing::map!

Integration

Test with real blockchain data

firecore, test fixtures

End-to-End

Validate complete pipeline

substreams CLI

Performance

Benchmark and optimize

Criterion, production mode

Start with comprehensive unit tests, add integration tests with real data, and validate with end-to-end tests. Automate everything through CI/CD.

Cargo.toml Requirements

Last updated

Was this helpful?