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!");
}
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);
}
最佳实践
- 测试隔离:每个测试使用独立的收集器实例
- 精确断言:尽量使用具体的字段匹配而不是通配符
- 性能考虑:在生产测试中合理配置过滤条件
- 错误处理:结合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验证、模式匹配、高级过滤以及异步日志测试。每个测试都使用了独立的收集器实例,遵循了最佳实践中的测试隔离原则。