Rust通用测试框架generic-tests的使用:高效编写与运行多场景单元测试和集成测试

Rust通用测试框架generic-tests的使用:高效编写与运行多场景单元测试和集成测试

概述

generic-tests是一个Rust过程宏属性宏,它允许测试编写者在使用相同测试协议但测试不同类型时重用测试代码。与Rust通用编程一样,这是通过使用泛型参数和trait边界实现的。具体的测试用例会在多个子模块中展开,类型参数由另一个属性提供。

特性

  • 为内置测试框架实例化测试和基准测试
  • 支持其他crate提供的任意测试函数属性
  • 可自定义的属性集从通用测试函数复制到其实例化
  • 支持async测试

使用示例

首先在Cargo.toml中添加依赖:

[dependencies]
generic-tests = "0.1.3"

然后是一个完整的示例demo:

use generic_tests::generic_test;

// 定义一个简单的trait用于测试
trait TestTrait {
    fn get_value(&self) -> i32;
}

// 实现TestTrait的结构体1
struct TestStruct1;
impl TestTrait for TestStruct1 {
    fn get_value(&self) -> i32 {
        42
    }
}

// 实现TestTrait的结构体2
struct TestStruct2;
impl TestTrait for TestStruct2 {
    fn get_value(&self) -> i32 {
        100
    }
}

// 定义通用测试函数
#[generic_test(T: TestTrait)]
#[test]
fn test_get_value(test_instance: T) {
    assert!(test_instance.get_value() > 0);
}

// 为具体类型实例化测试
#[instantiate_tests(<TestStruct1>)]
mod test_struct1 {}

#[instantiate_tests(<TestStruct2>)]
mod test_struct2 {}

在这个示例中:

  1. 我们定义了一个TestTrait trait和两个实现该trait的结构体
  2. 使用#[generic_test]宏定义了一个通用测试函数,它接受任何实现了TestTrait的类型
  3. 使用#[instantiate_tests]宏为TestStruct1TestStruct2分别实例化了测试模块
  4. 当运行测试时,会为每个类型运行test_get_value测试

高级用法

generic-tests还支持更复杂的场景,例如异步测试:

use generic_tests::generic_test;

trait AsyncTest {
    async fn async_op(&self) -> i32;
}

struct AsyncImpl1;
impl AsyncTest for AsyncImpl1 {
    async fn async_op(&self) -> i32 {
        10
    }
}

struct AsyncImpl2;
impl AsyncTest for AsyncImpl2 {
    async fn async_op(&self) -> i32 {
        20
    }
}

#[generic_test(T: AsyncTest)]
#[tokio::test]
async fn test_async_op(instance: T) {
    let result = instance.async_op().await;
    assert!(result > 0);
}

#[instantiate_tests(<AsyncImpl1>)]
mod async_impl1_tests {}

#[instantiate_tests(<AsyncImpl2>)]
mod async_impl2_tests {}

完整示例代码

下面是一个更完整的示例,展示如何在实际项目中使用generic-tests:

// 引入必要的宏
use generic_tests::{generic_test, instantiate_tests};

// 定义要测试的trait
trait Calculator {
    fn add(&self, a: i32, b: i32) -> i32;
    fn multiply(&self, a: i32, b: i32) -> i32;
}

// 实现1:简单计算器
struct SimpleCalculator;
impl Calculator for SimpleCalculator {
    fn add(&self, a: i32, b: i32) -> i32 {
        a + b
    }
    
    fn multiply(&self, a: i32, b: i32) -> i32 {
        a * b
    }
}

// 实现2:带日志的计算器
struct LoggingCalculator;
impl Calculator for LoggingCalculator {
    fn add(&self, a: i32, b: i32) -> i32 {
        println!("Adding {} and {}", a, b);
        a + b
    }
    
    fn multiply(&self, a: i32, b: i32) -> i32 {
        println!("Multiplying {} and {}", a, b);
        a * b
    }
}

// 定义通用测试
#[generic_test(T: Calculator)]
#[test]
fn test_add_operation(calculator: T) {
    assert_eq!(calculator.add(2, 3), 5);
    assert_eq!(calculator.add(-1, 1), 0);
}

#[generic_test(T: Calculator)]
#[test]
fn test_multiply_operation(calculator: T) {
    assert_eq!(calculator.multiply(2, 3), 6);
    assert_eq!(calculator.multiply(-1, 1), -1);
}

// 为每种实现实例化测试
#[instantiate_tests(<SimpleCalculator>)]
mod simple_calculator_tests {}

#[instantiate_tests(<LoggingCalculator>)]
mod logging_calculator_tests {}

许可证

本项目采用双重许可:

  • Apache License, Version 2.0
  • MIT license

您可以选择其中任意一种许可证。


1 回复

Rust通用测试框架generic-tests使用指南

概述

generic-tests是一个Rust测试框架,专注于简化多场景单元测试和集成测试的编写与运行。它通过泛型和宏提供了强大的测试组织能力,特别适合需要针对不同类型或多种输入参数运行相同测试逻辑的场景。

主要特性

  • 支持参数化测试(同一测试用例运行多组输入)
  • 简化测试组织和管理
  • 提供清晰的测试报告输出
  • 支持单元测试和集成测试
  • 与标准cargo test命令无缝集成

安装

在Cargo.toml中添加依赖:

[dev-dependencies]
generic-tests = "0.3"

基本使用方法

1. 简单测试示例

use generic_tests::test_case;

#[test_case]
fn test_addition() {
    assert_eq!(2 + 2, 4);
}

2. 参数化测试

use generic_tests::{test_case, test_matrix};

#[test_matrix(
    [(2, 2, 4), (3, 5, 8), (10, -2, 8)]
)]
fn test_addition(a: i32, b: i32, expected: i32) {
    assert_eq!(a + b, expected);
}

3. 泛型测试

use generic_tests::{test_case, generic_test};

trait Addable: std::ops::Add<Output = Self> + Sized + PartialEq + Copy {}
impl<T: std::ops::Add<Output = T> + PartialEq + Copy> Addable for T {}

#[generic_test]
fn test_generic_addition<T: Addable>(a: T, b: T, expected: T) {
    assert_eq!(a + b, expected);
}

// 为不同类型实例化测试
test_generic_addition!(i32: (2, 2, 4));
test_generic_addition!(f64: (2.5, 3.5, 6.0));

4. 集成测试

tests/目录下创建集成测试文件:

// tests/integration_test.rs
use generic_tests::test_case;
use my_crate::some_function;

#[test_case]
fn test_some_function() {
    assert!(some_function().is_ok());
}

高级用法

1. 测试前后设置

use generic_tests::{test_case, setup, teardown};

#[setup]
fn setup() {
    // 测试前的初始化代码
    println!("Setting up test");
}

#[teardown]
fn teardown() {
    // 测试后的清理代码
    println!("Tearing down test");
}

#[test_case]
fn test_with_setup() {
    // 这个测试会自动运行setup和teardown
    assert!(true);
}

2. 异步测试

use generic_tests::{test_case, async_test_case};

#[async_test_case]
async fn test_async_function() {
    let result = some_async_function().await;
    assert!(result.is_ok());
}

3. 测试过滤

运行特定测试:

cargo test test_addition  # 运行名称包含"test_addition"的测试

最佳实践

  1. 为每个功能模块创建对应的测试模块
  2. 使用参数化测试减少重复代码
  3. 将复杂测试分解为多个小测试
  4. 为测试使用有意义的名称
  5. 在集成测试中模拟真实使用场景

与标准测试框架对比

generic-tests在标准#[test]属性基础上增加了:

  • 更简洁的参数化测试语法
  • 内置泛型测试支持
  • 更好的测试组织能力
  • 更丰富的测试生命周期控制

但仍完全兼容标准测试框架的所有功能。

性能考虑

generic-tests在编译时会展开所有测试用例,运行时性能与标准测试框架相同。大量参数化测试可能会增加编译时间,但不会影响测试执行性能。

完整示例demo

下面是一个完整的示例,展示了如何使用generic-tests框架进行不同类型的测试:

// tests/example_tests.rs
use generic_tests::{test_case, test_matrix, generic_test, setup, teardown, async_test_case};

// 1. 简单测试示例
#[test_case]
fn test_simple_addition() {
    assert_eq!(2 + 2, 4);
}

// 2. 参数化测试
#[test_matrix(
    [(2, 2, 4), (3, 5, 8), (10, -2, 8)]
)]
fn test_parametrized_addition(a: i32, b: i32, expected: i32) {
    assert_eq!(a + b, expected);
}

// 3. 泛型测试
trait Addable: std::ops::Add<Output = Self> + Sized + PartialEq + Copy {}
impl<T: std::ops::Add<Output = T> + PartialEq + Copy> Addable for T {}

#[generic_test]
fn test_generic_add<T: Addable>(a: T, b: T, expected: T) {
    assert_eq!(a + b, expected);
}

test_generic_add!(i32: (2, 2, 4));
test_generic_add!(f64: (2.5, 3.5, 6.0));

// 4. 测试前后设置
#[setup]
fn global_setup() {
    println!("全局测试设置");
}

#[teardown]
fn global_teardown() {
    println!("全局测试清理");
}

#[test_case]
fn test_with_global_setup() {
    println!("运行测试");
    assert!(true);
}

// 5. 异步测试
#[async_test_case]
async fn test_async_operation() {
    async fn mock_async_fn() -> Result<(), &'static str> {
        Ok(())
    }
    
    let result = mock_async_fn().await;
    assert!(result.is_ok());
}

// 6. 模块内测试设置
mod module_tests {
    use super::*;
    
    #[setup]
    fn module_setup() {
        println!("模块测试设置");
    }
    
    #[teardown]
    fn module_teardown() {
        println!("模块测试清理");
    }
    
    #[test_case]
    fn test_with_module_setup() {
        println!("运行模块测试");
        assert!(true);
    }
}

这个完整示例展示了:

  1. 基本测试用例
  2. 参数化测试
  3. 泛型测试
  4. 全局测试设置和清理
  5. 异步测试支持
  6. 模块级别的测试设置

要运行这些测试,只需执行常规的cargo test命令:

cargo test

或者运行特定测试:

cargo test test_simple_addition
回到顶部