Rust日志测试库tracing-fluent-assertions的使用,实现高效tracing日志的断言和验证

Rust日志测试库tracing-fluent-assertions的使用,实现高效tracing日志的断言和验证

一个用于tracing的流畅断言框架。

概述

虽然已经有许多用于测试的crate——模拟、测试替身、高级断言等——但没有一个crate允许用户在整体层面上理解其tracing实现是如何被调用的。虽然有一些crate,如tracing-test,用于判断一段代码是否发出了某些事件,但没有通用的方式来询问以下问题:

  • 是否曾经创建或进入了span A?
  • 它是否曾经关闭?
  • 它是否至少进入/退出/关闭了N次?
  • 模块路径X中的任何span是否曾经被创建?

这就是tracing-fluent-assertions旨在解决的问题。

用法

这个crate与其他提供流畅断言的crate看起来并没有太大不同,但由于它面向的是由调用站点定义的span,与直接针对函数结果等定义断言相比,使用它需要一些样板代码。

首先,它提供了一个必须安装的Subscriber层,以便拦截span事件并跟踪span的生命周期。其次,提供了一个AssertionRegistry用于创建和存储断言。

一个Assertion定义了它应该匹配哪些span,以及span必须匹配什么行为才能成功断言。

一个简化的用法可能如下所示:

use tracing_fluent_assertions::{AssertionLayer, AssertionRegistry};
use tracing_subscriber::{layer::SubscriberExt, Registry};

fn main() {
    // 创建断言注册表并安装断言层,
    // 然后将该订阅者安装为全局默认值。
    let assertion_registry = AssertionRegistry::default();
    let base_subscriber = Registry::default();
    let subscriber = base_subscriber.with(AssertionsLayer::new(&assertion_registry));
    tracing::subscriber::set_global_default(subscriber).unwrap();

    // 创建一个断言。我们将寻找一个名为`shave_yak`的span,
    // 并断言它至少关闭了两次,表示该span的两个完整
    // 创建/进入/退出/关闭实例。本质上,至少
    // 有两头牦牛被完全剃毛。
    let more_than_one_shaved_yak = assertion_registry.build()
        .with_name("shave_yak")
        .was_closed_many(2)
        .finalize();

    // 现在,调用我们实际剃牦牛的方法。
    shave_yaks(5);

    // 假设所有五头牦牛都被剃了,这个断言将通过,
    // 并且不会产生panic,耶!
    more_than_one_yak_shaved.assert();

    // 断言的高级用法可以是确定一个span
    // 何时最终被进入。这对于确定异步函数
    // 何时已经通过其他等待点并且现在
    // 在我们控制的代码处等待(该代码有自己的span)非常有用。
    //
    // 为此,我们可以使用可失败的`try_assert`,如果
    // 断言标准尚未完全满足,它不会panic:
    let reached_acquire_shaving_shears = assertion_registry.build()
        .with_name("acquire_shaving_shears")
        .was_entered()
        .finalize();

    let manual_future = shave_yaks_async(5);

    assert!(!reached_acquire_shaving_shears.try_assert());
    while !reached_acquire_shaving_shears.try_assert() {
        manual_future.poll();
    }

    // 一旦我们跳出那个循环,我们就知道我们至少进入了一次
    // `acquire_shaving_shears` span。这个例子有点
    // 做作,但一个更有用的场景(尽管需要更多代码
    // 来演示)是确定一个异步任务何时最终
    // 等待一个特定资源,当它必须等待其他资源
    // 而在测试中无法确定性地控制时。
}

完整示例代码

use tracing_fluent_assertions::{AssertionLayer, AssertionRegistry};
use tracing_subscriber::{layer::SubscriberExt, Registry};
use tracing::{info_span, Instrument};

// 示例函数:同步剃牦牛
fn shave_yaks(count: u32) {
    for i in 1..=count {
        let span = info_span!("shave_yak", yak_id = i);
        let _enter = span.enter();
        // 模拟剃牦牛的工作
        println!("Shaving yak {}", i);
    }
}

// 示例函数:异步剃牦牛
async fn shave_yaks_async(count: u32) {
    for i in 1..=count {
        let span = info_span!("shave_yak", yak_id = i);
        let _enter = span.enter();
        // 模拟异步剃牦牛的工作
        println!("Shaving yak {} asynchronously", i);
        
        // 模拟获取剃毛剪的异步操作
        acquire_shaving_shears().await;
    }
}

// 模拟获取剃毛剪的异步函数
async fn acquire_shaving_shears() {
    let span = info_span!("acquire_shaving_shears");
    let _enter = span.enter();
    // 模拟异步获取工具
    println!("Acquiring shaving shears...");
}

#[tokio::main]
async fn main() {
    // 创建断言注册表并安装断言层
    let assertion_registry = AssertionRegistry::default();
    let base_subscriber = Registry::default();
    let subscriber = base_subscriber.with(AssertionLayer::new(&assertion_registry));
    tracing::subscriber::set_global_default(subscriber).unwrap();

    // 创建断言:检查shave_yak span是否至少关闭两次
    let more_than_one_shaved_yak = assertion_registry.build()
        .with_name("shave_yak")
        .was_closed_many(2)
        .finalize();

    // 调用同步剃牦牛函数
    shave_yaks(5);

    // 断言验证
    more_than_one_shaved_yak.assert();
    println!("Sync assertion passed!");

    // 创建异步断言:检查acquire_shaving_shears span是否被进入
    let reached_acquire_shaving_shears = assertion_registry.build()
        .with_name("acquire_shaving_shears")
        .was_entered()
        .finalize();

    // 调用异步剃牦牛函数
    let future = shave_yaks_async(3);
    
    // 使用try_assert进行非阻塞检查
    assert!(!reached_acquire_shaving_shears.try_assert());
    
    // 执行异步任务直到断言满足
    future.await;
    
    // 最终验证异步断言
    reached_acquire_shaving_shears.assert();
    println!("Async assertion passed!");
}

1 回复

Rust日志测试库tracing-fluent-assertions的使用指南

概述

tracing-fluent-assertions是一个专门用于Rust tracing日志系统的测试断言库,它提供了简洁的API来验证应用程序的日志输出是否符合预期。该库特别适合在单元测试和集成测试中验证tracing事件和span的行为。

主要特性

  • 支持对tracing事件和span的断言验证
  • 提供流畅的断言API
  • 支持字段值匹配和模式匹配
  • 可配置的日志收集和过滤

安装方法

在Cargo.toml中添加依赖:

[dev-dependencies]
tracing-fluent-assertions = "0.2"
tracing-subscriber = "0.3"

基本使用方法

1. 基本设置

use tracing_fluent_assertions::*;
use tracing_subscriber::prelude::*;

#[tokio::test]
async fn test_basic_logging() {
    // 设置日志收集器
    let collector = tracing_fluent_assertions::collector()
        .with_test_writer()
        .init();
    
    // 你的测试代码
    tracing::info!("User logged in: {}", "alice");
    
    // 断言验证
    assert_events!(collector)
        .with_level(tracing::Level::INFO)
        .with_target("your_crate_name")
        .with_fields(field("message").eq("User logged in: alice"))
        .count(1);
}

2. 字段断言示例

#[test]
fn test_field_assertions() {
    let collector = tracing_fluent_assertions::collector().init();
    
    tracing::info!(user_id = 123, action = "login", "User activity");
    
    assert_events!(collector)
        .with_fields(
            field("user_id").eq(123)
            .and(field("action").eq("login"))
        )
        .count(1);
}

3. Span断言示例

#[tokio::test]
async fn test_span_assertions() {
    let collector = tracing_fluent_assertions::collector().init();
    
    let span = tracing::info_span!("request_processing", method = "GET", path = "/api/users");
    let _guard = span.enter();
    
    tracing::info!("Processing request");
    
    assert_spans!(collector)
        .with_name("request_processing")
        .with_fields(
            field("method").eq("GET")
            .and(field("path").eq("/api/users"))
        )
        .count(1);
}

4. 模式匹配示例

#[test]
fn test_pattern_matching() {
    let collector = tracing_fluent_assertions::collector().init();
    
    tracing::warn!("Failed to connect to database: Connection timeout");
    
    assert_events!(collector)
        .with_level(tracing::Level::WARN)
        .with_fields(field("message").contains("database"))
        .count(1);
}

5. 高级过滤示例

#[test]
fn test_advanced_filtering() {
    let collector = tracing_fluent_assertions::collector()
        .with_filter(|metadata| {
            metadata.target().contains("database") || 
            metadata.level() >= &tracing::Level::WARN
        })
        .init();
    
    // 只有符合过滤条件的日志会被收集和断言
    tracing::debug!("Debug message"); // 不会被收集
    tracing::warn!("Database connection issue"); // 会被收集
    
    assert_events!(collector)
        .with_fields(field("message").contains("Database"))
        .count(1);
}

最佳实践

  1. 测试隔离:每个测试使用独立的收集器实例
  2. 精确断言:尽量使用具体的字段匹配而不是通配符
  3. 性能考虑:在生产测试中合理配置过滤条件
  4. 错误处理:结合Result类型进行更健壮的测试

常见问题

问: 如何测试异步代码中的日志?

答: 使用async测试函数并在断言前确保所有异步操作完成:

#[tokio::test]
async fn test_async_logging() {
    let collector = tracing_fluent_assertions::collector().init();
    
    tokio::spawn(async {
        tracing::info!("Async log message");
    }).await.unwrap();
    
    // 确保异步任务完成后再进行断言
    assert_events!(collector).count(1);
}

这个库极大地简化了tracing日志的测试工作,使得验证应用程序的日志行为变得更加简单和可靠。

完整示例demo

以下是一个完整的测试示例,展示了tracing-fluent-assertions库的主要功能:

// 引入必要的依赖
use tracing_fluent_assertions::*;
use tracing_subscriber::prelude::*;
use tracing::{info, warn, error, info_span};

// 用户服务结构体
struct UserService;

impl UserService {
    // 模拟用户登录方法
    fn login(&self, username: &str, user_id: u32) {
        info!(user_id = user_id, action = "login", "User {} logged in", username);
    }
    
    // 模拟处理请求方法
    async fn process_request(&self) {
        let span = info_span!("request_processing", method = "POST", path = "/api/login");
        let _guard = span.enter();
        
        info!("Processing login request");
        
        // 模拟一些处理逻辑
        tokio::time::sleep(std::time::Duration::from_millis(10)).await;
        
        info!("Request processed successfully");
    }
    
    // 模拟数据库错误方法
    fn database_error(&self) {
        warn!("Failed to connect to database: Connection timeout");
    }
}

// 基本日志测试
#[tokio::test]
async fn test_user_login() {
    // 初始化日志收集器
    let collector = tracing_fluent_assertions::collector()
        .with_test_writer()
        .init();
    
    let user_service = UserService;
    
    // 执行测试代码
    user_service.login("alice", 123);
    
    // 验证日志断言
    assert_events!(collector)
        .with_level(tracing::Level::INFO)
        .with_fields(
            field("user_id").eq(123)
            .and(field("action").eq("login"))
            .and(field("message").contains("alice"))
        )
        .count(1);
}

// Span测试
#[tokio::test]
async fn test_request_processing() {
    let collector = tracing_fluent_assertions::collector().init();
    
    let user_service = UserService;
    
    // 执行异步请求处理
    user_service.process_request().await;
    
    // 验证Span断言
    assert_spans!(collector)
        .with_name("request_processing")
        .with_fields(
            field("method").eq("POST")
            .and(field("path").eq("/api/login"))
        )
        .count(1);
    
    // 验证事件断言
    assert_events!(collector)
        .with_fields(field("message").contains("Processing login request"))
        .count(1);
}

// 模式匹配测试
#[test]
fn test_database_error() {
    let collector = tracing_fluent_assertions::collector().init();
    
    let user_service = UserService;
    user_service.database_error();
    
    // 使用模式匹配验证警告日志
    assert_events!(collector)
        .with_level(tracing::Level::WARN)
        .with_fields(field("message").contains("database"))
        .count(1);
}

// 高级过滤测试
#[test]
fn test_filtered_logging() {
    // 配置只收集WARN级别及以上或包含"database"目标的日志
    let collector = tracing_fluent_assertions::collector()
        .with_filter(|metadata| {
            metadata.target().contains("database") || 
            metadata.level() >= &tracing::Level::WARN
        })
        .init();
    
    // 这些日志不会被收集
    info!("This is an info message");
    info!(target: "app", "App info message");
    
    // 这些日志会被收集
    warn!("This is a warning message");
    error!("This is an error message");
    info!(target: "database", "Database query executed");
    
    // 验证收集到的日志数量
    assert_events!(collector).count(3);
}

// 异步日志测试
#[tokio::test]
async fn test_async_logging() {
    let collector = tracing_fluent_assertions::collector().init();
    
    // 在异步任务中生成日志
    let handle = tokio::spawn(async {
        info!("Log from async task");
        info!("Another log from async task");
    });
    
    // 等待异步任务完成
    handle.await.unwrap();
    
    // 验证异步日志被正确收集
    assert_events!(collector)
        .with_fields(field("message").contains("async"))
        .count(2);
}

// 复杂字段断言测试
#[test]
fn test_complex_field_assertions() {
    let collector = tracing_fluent_assertions::collector().init();
    
    // 记录包含多个字段的日志
    info!(
        user_id = 456,
        username = "bob",
        email = "bob@example.com",
        status = "active",
        "User registered successfully"
    );
    
    // 使用复杂的字段组合断言
    assert_events!(collector)
        .with_fields(
            field("user_id").eq(456)
            .and(field("username").eq("bob"))
            .and(field("email").contains("example.com"))
            .and(field("status").eq("active"))
            .and(field("message").eq("User registered successfully"))
        )
        .count(1);
}

这个完整的示例展示了tracing-fluent-assertions库的主要功能,包括基本日志断言、Span验证、模式匹配、高级过滤以及异步日志测试。每个测试都使用了独立的收集器实例,遵循了最佳实践中的测试隔离原则。

回到顶部