n0_tracing_test/lib.rs
1//! Helper functions and macros that allow for easier testing of crates that use `tracing`.
2//!
3//! The focus is on testing the logging, not on debugging the tests. That's why the
4//! library ensures that the logs do not depend on external state. For example, the
5//! `RUST_LOG` env variable is not used for log filtering.
6//!
7//! Similar crates:
8//!
9//! - [test-log](https://crates.io/crates/test-log): Initialize loggers before
10//! running tests
11//! - [tracing-fluent-assertions](https://crates.io/crates/tracing-fluent-assertions):
12//! More powerful assertions that also allow analyzing spans
13//!
14//! ## Usage
15//!
16//! This crate should mainly be used through the
17//! [`#[traced_test]`](attr.traced_test.html) macro.
18//!
19//! First, add a dependency on `n0-tracing-test` in `Cargo.toml`.
20//! Then, annotate your test function with the `#[traced_test]` macro.
21//!
22//! ```rust
23//! use tracing::{info, warn};
24//! use n0_tracing_test::traced_test;
25//!
26//! #[tokio::test]
27//! #[traced_test]
28//! async fn test_logs_are_captured() {
29//! // Local log
30//! info!("This is being logged on the info level");
31//!
32//! // Log from a spawned task (which runs in a separate thread)
33//! tokio::spawn(async {
34//! warn!("This is being logged on the warn level from a spawned task");
35//! })
36//! .await
37//! .unwrap();
38//!
39//! // Ensure that certain strings are or aren't logged
40//! assert!(logs_contain("logged on the info level"));
41//! assert!(logs_contain("logged on the warn level"));
42//! assert!(!logs_contain("logged on the error level"));
43//!
44//! // Ensure that the string `logged` is logged exactly twice
45//! logs_assert(|lines: &[&str]| {
46//! match lines.iter().filter(|line| line.contains("logged")).count() {
47//! 2 => Ok(()),
48//! n => Err(format!("Expected two matching logs, but found {}", n)),
49//! }
50//! });
51//! }
52//! ```
53//!
54//! Done! You can write assertions using one of two injected functions:
55//!
56//! - `logs_contain(&str) -> bool`: Use this within an `assert!` call to ensure
57//! that a certain string is (or isn't) logged anywhere in the logs.
58//! - `logs_assert(f: impl Fn(&[&str]) -> Result<(), String>)`: Run a function
59//! against the log lines. If the function returns an `Err`, panic. This can
60//! be used to run arbitrary assertion logic against the logs.
61//!
62//! Logs are written to stdout, so they are captured by the cargo test runner
63//! by default, but printed if the test fails.
64//!
65//! Of course, you can also annotate regular non-async tests:
66//!
67//! ```rust
68//! use tracing::info;
69//! use n0_tracing_test::traced_test;
70//!
71//! #[traced_test]
72//! #[test]
73//! fn plain_old_test() {
74//! assert!(!logs_contain("Logging from a non-async test"));
75//! info!("Logging from a non-async test");
76//! assert!(logs_contain("Logging from a non-async test"));
77//! assert!(!logs_contain("This was never logged"));
78//! }
79//! ```
80//!
81//! ## Rationale / Why You Need This
82//!
83//! Tracing allows you to set a default subscriber within a scope:
84//!
85//! ```rust
86//! # let subscriber = tracing::Dispatch::new(tracing_subscriber::FmtSubscriber::new());
87//! # let req = 123;
88//! # fn get_response(fake_req: u8) {}
89//! let response = tracing::dispatcher::with_default(&subscriber, || get_response(req));
90//! ```
91//!
92//! This works fine, as long as no threads are involved. As soon as you use a
93//! multi-threaded test runtime (e.g. the `#[tokio::test]` with the
94//! `rt-multi-thread` feature) and spawn tasks, the tracing logs in those tasks
95//! will not be captured by the subscriber.
96//!
97//! The macro provided in this crate registers a global default subscriber instead.
98//! This subscriber contains a writer which logs into a global static in-memory buffer.
99//!
100//! At the beginning of every test, the macro injects span opening code. The span
101//! uses the name of the test function (unless it's already taken, then a counter
102//! is appended). This means that the logs from a test are prefixed with the test
103//! name, which helps when debugging.
104//!
105//! Finally, a function called `logs_contain(value: &str)` is injected into every
106//! annotated test. It filters the logs in the buffer to include only lines
107//! containing ` {span_name}: ` and then searches the value in the matching log
108//! lines. This can be used to assert that a message was logged during a test.
109//!
110//! ## Per-crate Filtering
111//!
112//! By default, `tracing-test` sets an env filter that filters out all logs
113//! except the ones from your crate (equivalent to
114//! `RUST_LOG=<your_crate>=trace`). If you need to capture logs from other crates
115//! as well, you can turn off this log filtering globally by enabling the
116//! `no-env-filter` Cargo feature:
117//!
118//! ```toml
119//! tracing-test = { version = "0.1", features = ["no-env-filter"] }
120//! ```
121//!
122//! Note that this will result in _all_ logs from _all_ your dependencies being
123//! captured! This means that the `logs_contain` function may become less
124//! useful, and you might need to use `logs_assert` instead, with your own
125//! custom filtering logic.
126//!
127//! **Note:** Rust "integration tests" (in the `tests/` directory) are each
128//! built into a separate crate from the crate they test. As a result,
129//! integration tests must use `no-env-filter` to capture and observe logs.
130//!
131//! ## Controlling log printing
132//!
133//! To suppress all log output, even when tests are run with `--nocapture`,
134//! enable `no-log-printing` feature.
135//!
136//! To enable color output and support for filtering with `RUST_LOG` in the
137//! terminal output, enable the `pretty-log-printing` feature.
138
139pub mod internal;
140mod subscriber;
141
142pub use n0_tracing_test_macro::traced_test;