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)