Testing Aspects
Comprehensive strategies for testing custom aspects and aspect-enhanced functions.
Unit Testing Aspects
Test aspects in isolation to verify their behavior.
Testing Before/After Advice
#![allow(unused)]
fn main() {
use aspect_core::prelude::*;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_logging_aspect_before() {
let aspect = LoggingAspect::new();
let ctx = JoinPoint {
function_name: "test_function",
module_path: "test::module",
location: Location {
file: "test.rs",
line: 42,
column: 10,
},
};
// Capture output
let output = capture_output(|| {
aspect.before(&ctx);
});
assert!(output.contains("test_function"));
assert!(output.contains("[ENTRY]"));
}
#[test]
fn test_logging_aspect_after() {
let aspect = LoggingAspect::new();
let ctx = JoinPoint {
function_name: "test_function",
module_path: "test::module",
location: Location {
file: "test.rs",
line: 42,
column: 10,
},
};
let result: i32 = 42;
let boxed_result: Box<dyn Any> = Box::new(result);
let output = capture_output(|| {
aspect.after(&ctx, boxed_result.as_ref());
});
assert!(output.contains("test_function"));
assert!(output.contains("[EXIT]"));
}
#[test]
fn test_logging_aspect_error() {
let aspect = LoggingAspect::new();
let ctx = JoinPoint {
function_name: "test_function",
module_path: "test::module",
location: Location {
file: "test.rs",
line: 42,
column: 10,
},
};
let error = AspectError::execution("test error");
let output = capture_stderr(|| {
aspect.after_error(&ctx, &error);
});
assert!(output.contains("test_function"));
assert!(output.contains("test error"));
assert!(output.contains("[ERROR]"));
}
}
}
Testing Around Advice
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_retry_aspect_success_first_try() {
let aspect = RetryAspect::new(3, Duration::from_millis(10));
let mut call_count = 0;
let pjp = create_test_pjp(|| {
call_count += 1;
Ok(Box::new(42) as Box<dyn Any>)
});
let result = aspect.around(pjp);
assert!(result.is_ok());
assert_eq!(call_count, 1); // Success on first try
}
#[test]
fn test_retry_aspect_success_after_retries() {
let aspect = RetryAspect::new(3, Duration::from_millis(10));
let mut call_count = 0;
let pjp = create_test_pjp(|| {
call_count += 1;
if call_count < 3 {
Err(AspectError::execution("temporary failure"))
} else {
Ok(Box::new(42) as Box<dyn Any>)
}
});
let result = aspect.around(pjp);
assert!(result.is_ok());
assert_eq!(call_count, 3); // Success on third try
}
#[test]
fn test_retry_aspect_all_attempts_fail() {
let aspect = RetryAspect::new(3, Duration::from_millis(10));
let mut call_count = 0;
let pjp = create_test_pjp(|| {
call_count += 1;
Err(AspectError::execution("permanent failure"))
});
let result = aspect.around(pjp);
assert!(result.is_err());
assert_eq!(call_count, 3); // All attempts exhausted
}
// Helper to create test ProceedingJoinPoint
fn create_test_pjp<F>(f: F) -> ProceedingJoinPoint
where
F: FnOnce() -> Result<Box<dyn Any>, AspectError> + 'static,
{
ProceedingJoinPoint::new(
f,
&JoinPoint {
function_name: "test",
module_path: "test",
location: Location {
file: "test.rs",
line: 1,
column: 1,
},
},
)
}
}
}
Testing Stateful Aspects
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{Arc, Mutex};
#[test]
fn test_timing_aspect_records_duration() {
let timings = Arc::new(Mutex::new(Vec::new()));
let aspect = TimingAspect::with_callback({
let timings = timings.clone();
move |duration| {
timings.lock().unwrap().push(duration);
}
});
let ctx = create_test_joinpoint();
aspect.before(&ctx);
std::thread::sleep(Duration::from_millis(10));
aspect.after(&ctx, &Box::new(()));
let recorded = timings.lock().unwrap();
assert_eq!(recorded.len(), 1);
assert!(recorded[0] >= Duration::from_millis(10));
}
#[test]
fn test_metrics_aspect_counts_calls() {
let aspect = MetricsAspect::new();
let ctx = create_test_joinpoint();
// Simulate multiple calls
for _ in 0..5 {
aspect.before(&ctx);
aspect.after(&ctx, &Box::new(()));
}
let stats = aspect.get_stats("test_function");
assert_eq!(stats.call_count, 5);
assert_eq!(stats.success_count, 5);
assert_eq!(stats.error_count, 0);
}
#[test]
fn test_metrics_aspect_tracks_errors() {
let aspect = MetricsAspect::new();
let ctx = create_test_joinpoint();
// Successful calls
aspect.before(&ctx);
aspect.after(&ctx, &Box::new(()));
aspect.before(&ctx);
aspect.after(&ctx, &Box::new(()));
// Failed call
aspect.before(&ctx);
aspect.after_error(&ctx, &AspectError::execution("error"));
let stats = aspect.get_stats("test_function");
assert_eq!(stats.call_count, 3);
assert_eq!(stats.success_count, 2);
assert_eq!(stats.error_count, 1);
}
}
}
Integration Testing
Test functions with aspects applied.
Testing Aspect-Enhanced Functions
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
#[aspect(LoggingAspect::new())]
fn function_under_test(x: i32) -> Result<i32, String> {
if x < 0 {
Err("Negative input".to_string())
} else {
Ok(x * 2)
}
}
#[test]
fn test_function_success_case() {
let output = capture_output(|| {
let result = function_under_test(5);
assert_eq!(result, Ok(10));
});
// Verify logging occurred
assert!(output.contains("[ENTRY]"));
assert!(output.contains("[EXIT]"));
assert!(output.contains("function_under_test"));
}
#[test]
fn test_function_error_case() {
let stderr = capture_stderr(|| {
let result = function_under_test(-5);
assert!(result.is_err());
});
// Verify error logging occurred
assert!(stderr.contains("[ERROR]"));
assert!(stderr.contains("Negative input"));
}
}
}
Testing Multiple Aspects
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
#[aspect(LoggingAspect::new())]
#[aspect(TimingAspect::new())]
#[aspect(MetricsAspect::new())]
fn multi_aspect_function(x: i32) -> i32 {
x * 2
}
#[test]
fn test_all_aspects_execute() {
// Capture all outputs
let (stdout, stderr, result) = capture_all(|| {
multi_aspect_function(21)
});
assert_eq!(result, 42);
// Verify logging
assert!(stdout.contains("[ENTRY]"));
assert!(stdout.contains("[EXIT]"));
// Verify timing
assert!(stdout.contains("took"));
// Verify metrics (would need metrics API to check)
}
}
}
Testing Aspect Ordering
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
// Track execution order
static EXECUTION_ORDER: Mutex<Vec<String>> = Mutex::new(Vec::new());
struct OrderTrackingAspect {
name: String,
}
impl Aspect for OrderTrackingAspect {
fn before(&self, _ctx: &JoinPoint) {
EXECUTION_ORDER.lock().unwrap().push(format!("{}_before", self.name));
}
fn after(&self, _ctx: &JoinPoint, _result: &dyn Any) {
EXECUTION_ORDER.lock().unwrap().push(format!("{}_after", self.name));
}
}
#[aspect(OrderTrackingAspect { name: "A".into() })]
#[aspect(OrderTrackingAspect { name: "B".into() })]
#[aspect(OrderTrackingAspect { name: "C".into() })]
fn ordered_function() -> i32 {
EXECUTION_ORDER.lock().unwrap().push("function".into());
42
}
#[test]
fn test_aspect_execution_order() {
EXECUTION_ORDER.lock().unwrap().clear();
let result = ordered_function();
assert_eq!(result, 42);
let order = EXECUTION_ORDER.lock().unwrap();
assert_eq!(
*order,
vec![
"A_before",
"B_before",
"C_before",
"function",
"C_after",
"B_after",
"A_after",
]
);
}
}
}
Mock Aspects for Testing
Create mock aspects for testing without side effects.
Mock Logging Aspect
#![allow(unused)]
fn main() {
use std::sync::{Arc, Mutex};
#[derive(Clone)]
struct MockLoggingAspect {
logs: Arc<Mutex<Vec<LogEntry>>>,
}
struct LogEntry {
level: LogLevel,
message: String,
function_name: String,
}
impl MockLoggingAspect {
fn new() -> Self {
Self {
logs: Arc::new(Mutex::new(Vec::new())),
}
}
fn get_logs(&self) -> Vec<LogEntry> {
self.logs.lock().unwrap().clone()
}
fn clear(&self) {
self.logs.lock().unwrap().clear();
}
}
impl Aspect for MockLoggingAspect {
fn before(&self, ctx: &JoinPoint) {
self.logs.lock().unwrap().push(LogEntry {
level: LogLevel::Info,
message: format!("Entering {}", ctx.function_name),
function_name: ctx.function_name.to_string(),
});
}
fn after(&self, ctx: &JoinPoint, _result: &dyn Any) {
self.logs.lock().unwrap().push(LogEntry {
level: LogLevel::Info,
message: format!("Exiting {}", ctx.function_name),
function_name: ctx.function_name.to_string(),
});
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_with_mock_logging() {
let mock = MockLoggingAspect::new();
#[aspect(mock.clone())]
fn test_function(x: i32) -> i32 {
x * 2
}
let result = test_function(21);
assert_eq!(result, 42);
let logs = mock.get_logs();
assert_eq!(logs.len(), 2);
assert_eq!(logs[0].message, "Entering test_function");
assert_eq!(logs[1].message, "Exiting test_function");
}
}
}
Mock Circuit Breaker
#![allow(unused)]
fn main() {
struct MockCircuitBreaker {
should_fail: Arc<AtomicBool>,
call_count: Arc<AtomicUsize>,
}
impl MockCircuitBreaker {
fn new() -> Self {
Self {
should_fail: Arc::new(AtomicBool::new(false)),
call_count: Arc::new(AtomicUsize::new(0)),
}
}
fn set_should_fail(&self, fail: bool) {
self.should_fail.store(fail, Ordering::Relaxed);
}
fn get_call_count(&self) -> usize {
self.call_count.load(Ordering::Relaxed)
}
}
impl Aspect for MockCircuitBreaker {
fn before(&self, ctx: &JoinPoint) {
self.call_count.fetch_add(1, Ordering::Relaxed);
if self.should_fail.load(Ordering::Relaxed) {
panic!("Circuit breaker: {} - Circuit open", ctx.function_name);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_circuit_breaker_allows_when_closed() {
let cb = MockCircuitBreaker::new();
cb.set_should_fail(false);
#[aspect(cb.clone())]
fn test_fn() -> i32 {
42
}
let result = test_fn();
assert_eq!(result, 42);
assert_eq!(cb.get_call_count(), 1);
}
#[test]
#[should_panic(expected = "Circuit open")]
fn test_circuit_breaker_fails_when_open() {
let cb = MockCircuitBreaker::new();
cb.set_should_fail(true);
#[aspect(cb.clone())]
fn test_fn() -> i32 {
42
}
test_fn(); // Should panic
}
}
}
Property-Based Testing
Use property-based testing for comprehensive coverage.
Testing Aspect Invariants
#![allow(unused)]
fn main() {
use proptest::prelude::*;
proptest! {
#[test]
fn test_timing_aspect_always_positive(delay_ms in 0u64..100) {
let aspect = TimingAspect::new();
let ctx = create_test_joinpoint();
aspect.before(&ctx);
std::thread::sleep(Duration::from_millis(delay_ms));
aspect.after(&ctx, &Box::new(()));
let duration = aspect.get_last_duration();
prop_assert!(duration >= Duration::from_millis(delay_ms));
}
#[test]
fn test_retry_aspect_never_exceeds_max_attempts(
max_attempts in 1usize..10,
should_succeed_at in prop::option::of(0usize..10)
) {
let aspect = RetryAspect::new(max_attempts, Duration::from_millis(1));
let mut actual_attempts = 0;
let pjp = create_test_pjp(|| {
actual_attempts += 1;
if let Some(succeed_at) = should_succeed_at {
if actual_attempts >= succeed_at {
return Ok(Box::new(42) as Box<dyn Any>);
}
}
Err(AspectError::execution("fail"))
});
let _ = aspect.around(pjp);
prop_assert!(actual_attempts <= max_attempts);
}
}
}
Testing Async Aspects
Test aspects with async functions.
#![allow(unused)]
fn main() {
#[cfg(test)]
mod async_tests {
use super::*;
use tokio::test;
#[aspect(LoggingAspect::new())]
async fn async_function(x: i32) -> Result<i32, String> {
tokio::time::sleep(Duration::from_millis(10)).await;
Ok(x * 2)
}
#[tokio::test]
async fn test_async_function_with_aspect() {
let output = capture_output(|| async {
let result = async_function(21).await;
assert_eq!(result, Ok(42));
}).await;
assert!(output.contains("[ENTRY]"));
assert!(output.contains("[EXIT]"));
}
#[tokio::test]
async fn test_concurrent_async_calls() {
let handles: Vec<_> = (0..10)
.map(|i| {
tokio::spawn(async move {
async_function(i).await
})
})
.collect();
for (i, handle) in handles.into_iter().enumerate() {
let result = handle.await.unwrap();
assert_eq!(result, Ok(i * 2));
}
}
}
}
Test Helpers
Utility functions for testing aspects.
#![allow(unused)]
fn main() {
// Helper to create test JoinPoint
fn create_test_joinpoint() -> JoinPoint {
JoinPoint {
function_name: "test_function",
module_path: "test::module",
location: Location {
file: "test.rs",
line: 42,
column: 10,
},
}
}
// Helper to capture stdout
fn capture_output<F, R>(f: F) -> (String, R)
where
F: FnOnce() -> R,
{
use std::sync::Mutex;
static CAPTURED: Mutex<Vec<u8>> = Mutex::new(Vec::new());
let result = f();
let captured = CAPTURED.lock().unwrap();
let output = String::from_utf8_lossy(&captured).to_string();
(output, result)
}
// Helper to capture stderr
fn capture_stderr<F, R>(f: F) -> (String, R)
where
F: FnOnce() -> R,
{
// Similar implementation for stderr
}
}
Best Practices
- Test Aspects in Isolation: Unit test aspect behavior separately
- Test Integration: Verify aspects work with real functions
- Use Mocks: Create mock aspects for testing without side effects
- Test Ordering: Verify aspect execution order
- Test Error Cases: Ensure error handling works correctly
- Property-Based Testing: Use proptest for comprehensive coverage
- Async Testing: Test async functions with aspects
- Test Performance: Benchmark aspect overhead
Summary
Testing strategies covered:
- Unit Testing: Test aspects in isolation
- Integration Testing: Test with real functions
- Mock Aspects: Testing without side effects
- Property-Based Testing: Comprehensive coverage
- Async Testing: Testing async functions
- Test Helpers: Utility functions for testing
Key Takeaways:
- Always test aspects in isolation first
- Use mocks to avoid side effects in tests
- Test aspect ordering explicitly
- Property-based testing finds edge cases
- Async aspects need special test handling
Next Steps:
- See Case Studies for real-world examples
- Review Production Patterns for best practices
- Check Advanced Patterns for complex scenarios