Integration Testing
Testing Multiple Modules
While unit tests verify a single module (lexer, parser, or evaluator) in isolation, multi-module integration tests verify that data types, error conversions (From / ?), and state transitions flow cleanly across module boundaries when composed into a complete pipeline.
Multi-Module Pipeline Tested End-to-End:
Raw Input (&str)
|
v
[ Module 1: `lexer::tokenize` ] --> Result<Vec<Token>, LexError>
|
v
[ Module 2: `parser::parse` ] --> Result<Expr, ParseError>
|
v
[ Module 3: `eval::evaluate` ] --> Result<i64, EvalError>
Testing Cross-Module Pipelines and Unified Error Conversion
In a multi-module library, each subsystem often defines its own error type (LexError, ParseError, EvalError), while the crate root exposes a top-level PipelineError implementing From for each stage's error. Integration tests verify both successful multi-stage execution and accurate error propagation from every stage:
pub mod lexer {
#[derive(Debug, PartialEq, Eq)]
pub enum Token {
Number(i64),
Plus,
}
#[derive(Debug, PartialEq, Eq)]
pub struct LexError(pub String);
pub fn tokenize(input: &str) -> Result<Vec<Token>, LexError> {
let mut tokens = Vec::new();
for part in input.split_whitespace() {
match part {
"+" => tokens.push(Token::Plus),
num => {
let parsed = num
.parse::<i64>()
.map_err(|_| LexError(format!("invalid token: {num}")))?;
tokens.push(Token::Number(parsed));
}
}
}
Ok(tokens)
}
}
pub mod parser {
use super::lexer::Token;
#[derive(Debug, PartialEq, Eq)]
pub struct AddChain(pub Vec<i64>);
#[derive(Debug, PartialEq, Eq)]
pub enum ParseError {
EmptyExpression,
UnexpectedToken,
}
pub fn parse(tokens: &[Token]) -> Result<AddChain, ParseError> {
if tokens.is_empty() {
return Err(ParseError::EmptyExpression);
}
let mut numbers = Vec::new();
for (i, tok) in tokens.iter().enumerate() {
match (i % 2, tok) {
(0, Token::Number(n)) => numbers.push(*n),
(1, Token::Plus) => {}
_ => return Err(ParseError::UnexpectedToken),
}
}
if tokens.len() % 2 == 0 {
return Err(ParseError::UnexpectedToken);
}
Ok(AddChain(numbers))
}
}
pub mod eval {
use super::lexer::{self, LexError};
use super::parser::{self, ParseError};
#[derive(Debug, PartialEq, Eq)]
pub enum PipelineError {
Lex(LexError),
Parse(ParseError),
Overflow,
}
impl From<LexError> for PipelineError {
fn from(e: LexError) -> Self {
Self::Lex(e)
}
}
impl From<ParseError> for PipelineError {
fn from(e: ParseError) -> Self {
Self::Parse(e)
}
}
pub fn evaluate_str(source: &str) -> Result<i64, PipelineError> {
let tokens = lexer::tokenize(source)?;
let ast = parser::parse(&tokens)?;
ast.0
.into_iter()
.try_fold(0i64, |acc, x| acc.checked_add(x).ok_or(PipelineError::Overflow))
}
}
fn main() {
use eval::{PipelineError, evaluate_str};
use lexer::LexError;
use parser::ParseError;
// 1. Happy path across all three modules
assert_eq!(evaluate_str("10 + 20 + 35"), Ok(65));
// 2. Stage 1 error (Lexer) propagates as PipelineError::Lex
assert_eq!(
evaluate_str("10 + foo"),
Err(PipelineError::Lex(LexError("invalid token: foo".into())))
);
// 3. Stage 2 error (Parser) propagates as PipelineError::Parse
assert_eq!(
evaluate_str("10 + + 20"),
Err(PipelineError::Parse(ParseError::UnexpectedToken))
);
// 4. Stage 3 error (Evaluator overflow) propagates as PipelineError::Overflow
assert_eq!(
evaluate_str(&format!("{} + 1", i64::MAX)),
Err(PipelineError::Overflow)
);
println!("Multi-module pipeline verified: 10 + 20 + 35 = {:?}", evaluate_str("10 + 20 + 35"));
}
Multi-module pipeline verified: 10 + 20 + 35 = Ok(65)