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 firecore 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 Criterion 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
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?

