Skip to main content

debtmap/testing/
mod.rs

1//! Test quality analysis and anti-pattern detection.
2//!
3//! This module analyzes test code to detect quality issues and anti-patterns
4//! that reduce test reliability and maintainability. It identifies tests
5//! without assertions, overly complex tests, and flaky test patterns.
6//!
7//! # Anti-Patterns Detected
8//!
9//! - **Tests without assertions**: Tests that don't verify anything
10//! - **Overly complex tests**: Tests with excessive mocking or setup
11//! - **Flaky patterns**: Timing dependencies, random values, external deps
12//!
13//! # Quality Assessment
14//!
15//! Each detected pattern is assessed for its impact on test suite reliability
16//! and prioritized for remediation.
17
18pub mod assertion_detector;
19pub mod complexity_detector;
20pub mod flaky_detector;
21pub mod rust;
22pub mod timing_classifier;
23
24use crate::core::{DebtItem, DebtType, Priority};
25use std::path::{Path, PathBuf};
26use syn::{File, ItemFn};
27
28#[derive(Debug, Clone, PartialEq)]
29pub enum TestingAntiPattern {
30    TestWithoutAssertions {
31        test_name: String,
32        file: PathBuf,
33        line: usize,
34        has_setup: bool,
35        has_action: bool,
36        suggested_assertions: Vec<String>,
37    },
38    OverlyComplexTest {
39        test_name: String,
40        file: PathBuf,
41        line: usize,
42        complexity_score: u32,
43        complexity_sources: Vec<ComplexitySource>,
44        suggested_simplification: TestSimplification,
45    },
46    FlakyTestPattern {
47        test_name: String,
48        file: PathBuf,
49        line: usize,
50        flakiness_type: FlakinessType,
51        reliability_impact: ReliabilityImpact,
52        stabilization_suggestion: String,
53    },
54}
55
56#[derive(Debug, Clone, PartialEq)]
57pub enum ComplexitySource {
58    ExcessiveMocking,
59    NestedConditionals,
60    MultipleAssertions,
61    LoopInTest,
62    ExcessiveSetup,
63}
64
65#[derive(Debug, Clone, PartialEq)]
66pub enum TestSimplification {
67    ExtractHelper,
68    SplitTest,
69    ParameterizeTest,
70    SimplifySetup,
71    ReduceMocking,
72}
73
74#[derive(Debug, Clone, PartialEq)]
75pub enum FlakinessType {
76    TimingDependency,
77    RandomValues,
78    ExternalDependency,
79    FilesystemDependency,
80    NetworkDependency,
81    ThreadingIssue,
82}
83
84#[derive(Debug, Clone, PartialEq)]
85pub enum ReliabilityImpact {
86    Critical,
87    High,
88    Medium,
89    Low,
90}
91
92#[derive(Debug, Clone, PartialEq)]
93pub enum TestQualityImpact {
94    Critical,
95    High,
96    Medium,
97    Low,
98}
99
100pub trait TestingDetector {
101    fn detect_anti_patterns(&self, file: &File, path: &Path) -> Vec<TestingAntiPattern>;
102    fn detector_name(&self) -> &'static str;
103    fn assess_test_quality_impact(&self, pattern: &TestingAntiPattern) -> TestQualityImpact;
104}
105
106pub fn is_test_function(function: &ItemFn) -> bool {
107    function.attrs.iter().any(|attr| {
108        // Check if it's a test attribute
109        let path_str = attr
110            .path()
111            .segments
112            .iter()
113            .map(|seg| seg.ident.to_string())
114            .collect::<Vec<_>>()
115            .join("::");
116
117        // Match common test attributes
118        path_str == "test"
119            || path_str == "tokio::test"
120            || path_str == "async_std::test"
121            || path_str == "bench"
122            || path_str.ends_with("::test")
123    }) || function.sig.ident.to_string().starts_with("test_")
124        || function.sig.ident.to_string().ends_with("_test")
125}
126
127pub fn analyze_testing_patterns(file: &File, path: &Path) -> Vec<DebtItem> {
128    let detectors: Vec<Box<dyn TestingDetector>> = vec![
129        Box::new(assertion_detector::AssertionDetector::new()),
130        Box::new(complexity_detector::TestComplexityDetector::new()),
131        Box::new(flaky_detector::FlakyTestDetector::new()),
132    ];
133
134    let mut testing_items = Vec::new();
135
136    for detector in detectors {
137        let anti_patterns = detector.detect_anti_patterns(file, path);
138
139        for pattern in anti_patterns {
140            let impact = detector.assess_test_quality_impact(&pattern);
141            let debt_item = convert_testing_pattern_to_debt_item(pattern, impact, path);
142            testing_items.push(debt_item);
143        }
144    }
145
146    testing_items
147}
148
149fn convert_testing_pattern_to_debt_item(
150    pattern: TestingAntiPattern,
151    _impact: TestQualityImpact,
152    path: &Path,
153) -> DebtItem {
154    let (priority, message, context, line, debt_type) = match pattern {
155        TestingAntiPattern::TestWithoutAssertions {
156            test_name,
157            suggested_assertions,
158            line,
159            ..
160        } => (
161            Priority::High,
162            format!("Test '{}' has no assertions", test_name),
163            Some(format!(
164                "Add assertions: {}",
165                suggested_assertions.join(", ")
166            )),
167            line,
168            DebtType::TestQuality { issue_type: None },
169        ),
170        TestingAntiPattern::OverlyComplexTest {
171            test_name,
172            complexity_score,
173            suggested_simplification,
174            line,
175            ..
176        } => (
177            Priority::Medium,
178            format!(
179                "Test '{}' is overly complex (score: {})",
180                test_name, complexity_score
181            ),
182            Some(format!("Consider: {:?}", suggested_simplification)),
183            line,
184            DebtType::TestComplexity {
185                cyclomatic: 0,
186                cognitive: 0,
187            },
188        ),
189        TestingAntiPattern::FlakyTestPattern {
190            test_name,
191            flakiness_type,
192            stabilization_suggestion,
193            line,
194            ..
195        } => (
196            Priority::High,
197            format!(
198                "Test '{}' has flaky pattern: {:?}",
199                test_name, flakiness_type
200            ),
201            Some(stabilization_suggestion),
202            line,
203            DebtType::TestQuality { issue_type: None },
204        ),
205    };
206
207    DebtItem {
208        id: format!("testing-{}-{}", path.display(), line),
209        debt_type,
210        priority,
211        file: path.to_path_buf(),
212        line,
213        column: None,
214        message,
215        context,
216    }
217}