Running Tests
Comprehensive testing is a core principle of this project. Here’s how to run and write tests.
Quick Start
Run all tests:
cargo test
Expected output:
running 178 tests
...
test result: ok. 178 passed; 0 failed; 0 ignored
Test Commands
Run All Tests
cargo test
Run Tests with Output
See println! output from tests:
cargo test -- --nocapture
Run Specific Test
# By name
cargo test test_publish_decode
# By partial name
cargo test publish
# By module
cargo test codec::
Run Tests in Specific Module
# Codec tests only
cargo test --package mqtt-broker --lib codec::tests
# Topic matcher tests
cargo test --package mqtt-broker --lib topic_matcher::tests
# Session tests
cargo test --package mqtt-broker --lib session::tests
Run Tests in Release Mode
cargo test --release
Run Ignored Tests
Some tests are marked #[ignore] (slow or require setup):
cargo test -- --ignored
Run All Including Ignored
cargo test -- --include-ignored
Test Organization
src/
├── codec/
│ └── tests.rs # 60 tests
├── topic_matcher/
│ └── tests.rs # 23 tests
├── session/
│ └── tests.rs # 24 tests
├── router/
│ └── tests.rs # 24 tests
├── persistence/
│ └── tests.rs # 20 tests
├── transport/
│ └── tests.rs # 16 tests
└── server/
└── tests.rs # 11 tests
tests/
├── codec_proptest.rs # Property-based tests
└── message_test.rs # Integration tests
Test Categories
Unit Tests
Test individual functions and structs:
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_encode_decode_roundtrip() {
let packet = ConnectPacket {
client_id: "test".to_string(),
// ...
};
let mut buf = BytesMut::new();
MqttEncoder::encode(&MqttPacket::Connect(packet.clone()), &mut buf).unwrap();
let decoded = MqttDecoder::decode(&mut buf).unwrap().unwrap();
assert_eq!(decoded, MqttPacket::Connect(packet));
}
}
}
Integration Tests
Test multiple components together (in tests/ directory):
#![allow(unused)]
fn main() {
// tests/message_test.rs
use mqtt_broker::*;
#[tokio::test]
async fn test_publish_flow() {
let router = Router::new();
let session = Session::new("client-1", false);
// Subscribe
router.subscribe("client-1", "sensors/#", QoS::AtLeastOnce);
// Publish
let message = PublishPacket {
topic: "sensors/temp".to_string(),
payload: b"25".to_vec(),
// ...
};
let deliveries = router.route(&message);
assert_eq!(deliveries.len(), 1);
}
}
Property-Based Tests
Using proptest for randomized testing:
#![allow(unused)]
fn main() {
// tests/codec_proptest.rs
use proptest::prelude::*;
proptest! {
#[test]
fn test_varint_roundtrip(value in 0u32..268435455) {
let mut buf = BytesMut::new();
encode_variable_int(value, &mut buf).unwrap();
let decoded = decode_variable_int(&mut buf).unwrap();
prop_assert_eq!(value, decoded);
}
}
}
Run property tests:
cargo test proptest
Async Tests
Using #[tokio::test] for async code:
#![allow(unused)]
fn main() {
#[tokio::test]
async fn test_server_accept() {
let server = MqttServer::bind("127.0.0.1:0").await.unwrap();
let addr = server.local_addr();
tokio::spawn(async move {
server.run().await.unwrap();
});
let client = TcpStream::connect(addr).await.unwrap();
assert!(client.peer_addr().is_ok());
}
}
Test Coverage
Using cargo-tarpaulin
# Install
cargo install cargo-tarpaulin
# Run coverage
cargo tarpaulin --out Html
# Open report
open tarpaulin-report.html
Using llvm-cov
# Install
rustup component add llvm-tools-preview
cargo install cargo-llvm-cov
# Run coverage
cargo llvm-cov --html
# Open report
open target/llvm-cov/html/index.html
Benchmarks
Criterion Benchmarks
# Run all benchmarks
cargo bench
# Run specific benchmark
cargo bench publish
Benchmark location: benches/
Example benchmark:
#![allow(unused)]
fn main() {
// benches/codec_bench.rs
use criterion::{criterion_group, criterion_main, Criterion};
use mqtt_broker::codec::*;
fn bench_publish_encode(c: &mut Criterion) {
let packet = PublishPacket {
topic: "sensors/temperature".to_string(),
payload: vec![0u8; 1024],
// ...
};
c.bench_function("publish_encode_1kb", |b| {
b.iter(|| {
let mut buf = BytesMut::with_capacity(2048);
MqttEncoder::encode(&MqttPacket::Publish(packet.clone()), &mut buf)
})
});
}
criterion_group!(benches, bench_publish_encode);
criterion_main!(benches);
}
Writing New Tests
Test Naming Convention
#![allow(unused)]
fn main() {
#[test]
fn test_<module>_<function>_<scenario>() {
// ...
}
// Examples:
fn test_codec_publish_empty_payload() { }
fn test_topic_filter_multi_wildcard() { }
fn test_session_clean_session_true() { }
}
Test Structure (AAA Pattern)
#![allow(unused)]
fn main() {
#[test]
fn test_subscribe_adds_to_trie() {
// Arrange
let mut router = Router::new();
// Act
router.subscribe("client-1", "sensors/#", QoS::AtLeastOnce);
// Assert
let matches = router.matches("sensors/temp");
assert_eq!(matches.len(), 1);
assert_eq!(matches[0].client_id, "client-1");
}
}
Testing Error Cases
#![allow(unused)]
fn main() {
#[test]
fn test_invalid_topic_filter_returns_error() {
let result = TopicFilter::parse("sensors/#/invalid");
assert!(result.is_err());
assert!(matches!(result, Err(TopicError::InvalidFilter(_))));
}
}
Testing Async Code
#![allow(unused)]
fn main() {
#[tokio::test]
async fn test_connection_timeout() {
let result = tokio::time::timeout(
Duration::from_millis(100),
Connection::connect("127.0.0.1:9999")
).await;
assert!(result.is_err()); // Timeout
}
}
CI Integration
Tests run automatically on GitHub Actions:
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- run: cargo test --all-features
Troubleshooting
Tests Hang
Usually a deadlock or missing .await:
# Run with timeout
timeout 60 cargo test
# Run single-threaded
cargo test -- --test-threads=1
Flaky Tests
Add retry or increase timeouts:
#![allow(unused)]
fn main() {
#[tokio::test(flavor = "multi_thread")]
async fn test_concurrent_publishes() {
// Use tokio::time::timeout
let result = tokio::time::timeout(
Duration::from_secs(5),
async_operation()
).await;
assert!(result.is_ok());
}
}
Port Already in Use
Tests using network ports can conflict:
#![allow(unused)]
fn main() {
// Use port 0 for automatic assignment
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
}
Debug Failing Tests
# Verbose output
cargo test test_name -- --nocapture
# With RUST_BACKTRACE
RUST_BACKTRACE=1 cargo test test_name
# With logging
RUST_LOG=debug cargo test test_name -- --nocapture