Skip to content

Commit 7abee0e

Browse files
committed
refactor: implement dynamic edge case tests with improved organization
- Split integration tests into focused test files (basic_filter_tests.rs, edge_case_tests.rs) - Replace static test data with dynamic generation based on current time - Add comprehensive boundary testing for 7-day and 90-day filtering rules - Implement self-documenting test cases with clear business rule descriptions - Enhance test output formatting with aligned columns for better - readability
1 parent 679dd4c commit 7abee0e

File tree

4 files changed

+252
-97
lines changed

4 files changed

+252
-97
lines changed

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,20 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and
88

99
## [Unreleased]
1010

11+
### Changed
12+
- **Test Organization**: Split integration tests into focused test files
13+
- `tests/basic_filter_tests.rs` - Core integration tests with static test data
14+
- `tests/edge_case_tests.rs` - Dynamic boundary condition tests
15+
- **Test Infrastructure Enhancement**: Implemented dynamic test data generation
16+
- Added comprehensive boundary testing for time-based filtering rules
17+
- Self-documenting test cases with clear business rule descriptions
18+
- **Enhanced Test Reporting**: Added aligned, formatted test output for better readability
19+
20+
### Technical
21+
- Improved test maintainability with descriptive entity names and test descriptions
22+
23+
---
24+
1125
## [0.2.0] – 2025-06-28
1226

1327
### Added

README.md

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ src/
2929
├── main.rs ← Lambda entry point & business logic
3030
├── domain.rs ← Domain entities (Action, Priority)
3131
tests/
32-
├── integration_tests.rs ← End-to-end lambda testing
32+
├── basic_filter_tests.rs ← Core integration tests with static data
33+
├── edge_case_tests.rs ← Dynamic boundary condition tests
3334
testdata/
3435
├── *.json ← Test input files
3536
scripts/
@@ -82,18 +83,40 @@ The Lambda applies these filtering and processing rules:
8283
- Deduplication behavior with priority conflicts
8384
- Date parsing and filtering logic
8485

85-
### Integration Tests (`tests/integration_tests.rs`)
86+
### Integration Tests
87+
88+
**Basic Integration Tests (`tests/basic_filter_tests.rs`)**
8689
- **Real lambda execution** using `cargo lambda invoke`
8790
- **End-to-end validation** from JSON input to JSON output
8891
- **Error handling** verification (invalid enum variants)
8992
- **Order-agnostic testing** for robust HashMap-based results
9093

94+
**Dynamic Edge Case Tests (`tests/edge_case_tests.rs`)**
95+
- **Boundary condition testing** with dynamic test data generation
96+
- **Comprehensive business rule validation** (7-day/90-day boundaries)
97+
- **Self-documenting test cases** with clear descriptions
98+
- **Time-independent testing** that works regardless of execution date
99+
91100
**Test Data Files:**
92101
- `01_sample-input.json` - Basic filtering and deduplication
93102
- `02_priority-input.json` - Priority sorting validation
94103
- `03_bad-input.json` - Error handling (invalid priority variant)
95104
- `04_edge-cases.json` - Boundary conditions and complex scenarios
96105

106+
### Running Tests
107+
108+
```bash
109+
# Run all tests
110+
cargo test
111+
112+
# Run specific test suites
113+
cargo test --test basic_filter_tests # Basic integration tests
114+
cargo test --test edge_case_tests # Dynamic edge case tests
115+
116+
# Run unit tests only
117+
cargo test --lib
118+
```
119+
97120
## 🚀 Usage
98121

99122
### Prerequisites
@@ -222,4 +245,4 @@ This project serves as a reference implementation demonstrating:
222245
- **Integration testing** strategies for AWS Lambda functions
223246
- **Error handling** patterns in serverless Rust applications
224247
- **Clean Architecture** with clear separation of concerns
225-
- **Containerized development workflows** for Rust projects
248+
- **Containerized development workflows** for Rust projects

tests/integration_tests.rs renamed to tests/basic_filter_tests.rs

Lines changed: 0 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -177,100 +177,6 @@ fn test_bad_input_integration() -> Result<()> {
177177
Ok(())
178178
}
179179

180-
#[test]
181-
fn test_edge_case_filtering() -> Result<()> {
182-
// ---
183-
// Test edge cases around the 7-day and 90-day boundaries, plus deduplication
184-
let actions = run_lambda_invoke("testdata/04_edge-cases.json")?;
185-
186-
// Based on the test data (assuming current date around 2025-06-27):
187-
// - exactly_7_days: last_action 2025-06-20 (exactly 7 days ago) -> SHOULD PASS
188-
// - exactly_90_days: next_action 2025-09-25 (exactly 90 days away) -> SHOULD PASS
189-
// - just_over_7_days: last_action 2025-06-19 (8 days ago) -> SHOULD PASS
190-
// - just_under_90_days: next_action 2025-09-24 (89 days away) -> SHOULD PASS
191-
// - duplicate_entity (urgent): first occurrence -> SHOULD BE DEDUPLICATED OUT
192-
// - duplicate_entity (normal): second occurrence -> SHOULD PASS (last one wins)
193-
// - too_recent: last_action 2025-06-21 (6 days ago) -> SHOULD BE FILTERED OUT
194-
// - too_far_future: next_action 2025-09-26 (exactly 90 days away) -> SHOULD PASS
195-
196-
// We expect 6 actions after filtering and deduplication:
197-
// 8 input - 1 too_recent (filtered) - 1 duplicate removed = 6
198-
ensure!(
199-
actions.len() == 6,
200-
"Expected 6 actions to pass edge case filters, got {}",
201-
actions.len()
202-
);
203-
204-
// Verify the specific actions that should pass
205-
let entity_ids: Vec<&String> = actions.iter().map(|a| &a.entity_id).collect();
206-
let expected_ids = vec![
207-
"exactly_7_days",
208-
"exactly_90_days",
209-
"just_over_7_days",
210-
"just_under_90_days",
211-
"duplicate_entity",
212-
"too_far_future", // <-- included because it's exactly 90 days and code uses `<=`
213-
];
214-
215-
for expected_id in &expected_ids {
216-
ensure!(
217-
entity_ids.iter().any(|id| id == expected_id),
218-
"Expected to find entity_id '{}' in results, but didn't",
219-
expected_id
220-
);
221-
}
222-
223-
// Verify the filtered out actions are not present
224-
let filtered_ids = vec!["too_recent"];
225-
for filtered_id in &filtered_ids {
226-
ensure!(
227-
!entity_ids.iter().any(|id| id == filtered_id),
228-
"Expected entity_id '{}' to be filtered out, but found it in results",
229-
filtered_id
230-
);
231-
}
232-
233-
// Verify deduplication: duplicate_entity should appear exactly once
234-
let duplicate_count = entity_ids.iter().filter(|&id| *id == "duplicate_entity").count();
235-
ensure!(
236-
duplicate_count == 1,
237-
"Expected exactly 1 occurrence of 'duplicate_entity', found {}",
238-
duplicate_count
239-
);
240-
241-
// Verify that the duplicate_entity kept the LAST occurrence (normal priority)
242-
let duplicate_action = actions.iter().find(|a| a.entity_id == "duplicate_entity").unwrap();
243-
ensure!(
244-
duplicate_action.priority == Priority::Normal,
245-
"Expected duplicate_entity to keep the last occurrence (normal), but got {:?}",
246-
duplicate_action.priority
247-
);
248-
249-
// Verify priority sorting (urgent before normal)
250-
let mut seen_normal = false;
251-
for action in &actions {
252-
if action.priority == Priority::Normal {
253-
seen_normal = true;
254-
} else if action.priority == Priority::Urgent && seen_normal {
255-
panic!("Found urgent priority after normal priority - sorting is incorrect");
256-
}
257-
}
258-
259-
println!("Edge case filtering test passed:");
260-
println!(" {} actions passed the time filters", actions.len());
261-
println!(" Deduplication verified: duplicate_entity kept last occurrence (normal)");
262-
for (i, action) in actions.iter().enumerate() {
263-
println!(
264-
" {}. {} ({})",
265-
i + 1,
266-
action.entity_id,
267-
if action.priority == Priority::Urgent { "urgent" } else { "normal" }
268-
);
269-
}
270-
271-
Ok(())
272-
}
273-
274180
#[test]
275181
fn test_empty_input_array() -> Result<()> {
276182
// ---

tests/edge_case_tests.rs

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
use anyhow::{ensure, Result};
2+
use aws_lambda_action_filter::{Action, Priority};
3+
use chrono::{Duration, Utc};
4+
use serde_json::{self, Value};
5+
use std::collections::HashMap;
6+
use std::fs;
7+
use std::process::Command;
8+
9+
/// Helper function to run cargo lambda invoke and parse the result
10+
fn run_lambda_invoke(data_file: &str) -> Result<Vec<Action>> {
11+
// ---
12+
let output =
13+
Command::new("cargo").args(["lambda", "invoke", "--data-file", data_file]).output()?;
14+
15+
ensure!(
16+
output.status.success(),
17+
"cargo lambda invoke failed with status: {}\nstderr: {}",
18+
output.status,
19+
String::from_utf8_lossy(&output.stderr)
20+
);
21+
22+
let stdout = String::from_utf8(output.stdout)?;
23+
24+
// The output should be a JSON array of actions
25+
let json_value: Value = serde_json::from_str(&stdout)?;
26+
let actions: Vec<Action> = serde_json::from_value(json_value)?;
27+
28+
Ok(actions)
29+
}
30+
31+
struct TestCase {
32+
entity_id: &'static str,
33+
next_offset: i64, // Days from now (positive = future)
34+
last_offset: i64, // Days from now (negative = past)
35+
priority: Priority,
36+
should_pass: bool, // Expected to pass filtering?
37+
description: &'static str,
38+
}
39+
40+
#[rustfmt::skip]
41+
const EDGE_CASES: &[TestCase] = &[
42+
TestCase { entity_id: "dedup_first_occurrence", next_offset: 30, last_offset: -10, priority: Priority::Urgent, should_pass: true, description: "Tests deduplication (first occurrence)" },
43+
TestCase { entity_id: "dedup_first_occurrence", next_offset: 35, last_offset: -15, priority: Priority::Normal, should_pass: true, description: "Tests deduplication (last occurrence wins)" },
44+
TestCase { entity_id: "more_than_7_days_ago_fail", next_offset: 20, last_offset: -7, priority: Priority::Urgent, should_pass: false, description: "Tests 'more than 7 days ago' rule (should fail)" },
45+
TestCase { entity_id: "more_than_7_days_ago_pass", next_offset: 20, last_offset: -8, priority: Priority::Urgent, should_pass: true, description: "Tests 'more than 7 days ago' rule (should pass)" },
46+
TestCase { entity_id: "more_than_7_days_ago_pass_2", next_offset: 25, last_offset: -10, priority: Priority::Urgent, should_pass: true, description: "Tests 'more than 7 days ago' rule (should pass)" },
47+
TestCase { entity_id: "within_90_days_fail", next_offset: 91, last_offset: -30, priority: Priority::Normal, should_pass: false, description: "Tests 'within 90 days' rule (should fail at 91 days)" },
48+
TestCase { entity_id: "within_90_days_pass", next_offset: 90, last_offset: -30, priority: Priority::Normal, should_pass: true, description: "Tests 'within 90 days' rule boundary (should pass)" },
49+
TestCase { entity_id: "within_90_days_pass_2", next_offset: 89, last_offset: -20, priority: Priority::Normal, should_pass: true, description: "Tests 'within 90 days' rule (should pass)" },
50+
];
51+
52+
fn create_action(
53+
entity_id: &str,
54+
last_offset: i64,
55+
next_offset: i64,
56+
priority: Priority,
57+
) -> Action {
58+
// ---
59+
let now = Utc::now();
60+
Action {
61+
entity_id: entity_id.to_string(),
62+
last_action_time: now + Duration::days(last_offset),
63+
next_action_time: now + Duration::days(next_offset),
64+
priority,
65+
}
66+
}
67+
68+
fn generate_test_data() -> Result<String> {
69+
// ---
70+
let actions: Vec<Action> = EDGE_CASES
71+
.iter()
72+
.map(|test_case| {
73+
// ---
74+
create_action(
75+
test_case.entity_id,
76+
test_case.last_offset,
77+
test_case.next_offset,
78+
test_case.priority.clone(),
79+
)
80+
})
81+
.collect();
82+
83+
let json = serde_json::to_string_pretty(&actions)?;
84+
Ok(json)
85+
}
86+
87+
fn verify_test_expectations(results: &[Action]) -> Result<()> {
88+
// ---
89+
let prefix = "verify_test_expectations";
90+
91+
// Convert results to a map for O(1) lookup
92+
let result_map: HashMap<&str, &Action> =
93+
results.iter().map(|action| (action.entity_id.as_str(), action)).collect();
94+
95+
// Iterate over test expectations and verify against results
96+
for test_case in EDGE_CASES {
97+
// ---
98+
let found_in_results = result_map.contains_key(test_case.entity_id);
99+
100+
match (test_case.should_pass, found_in_results) {
101+
// ---
102+
(true, false) => {
103+
// ---
104+
ensure!(
105+
false,
106+
"{prefix}: {} - Expected to pass but was filtered out. {}",
107+
test_case.entity_id,
108+
test_case.description
109+
);
110+
}
111+
(false, true) => {
112+
// ---
113+
ensure!(
114+
false,
115+
"{prefix}: {} - Expected to be filtered out but found in results. {}",
116+
test_case.entity_id,
117+
test_case.description
118+
);
119+
}
120+
(true, true) => {
121+
// ---
122+
println!("✓ {:<28}: PASS - {}", test_case.entity_id, test_case.description);
123+
}
124+
(false, false) => {
125+
// ---
126+
println!("✓ {:<28}: FILTERED - {}", test_case.entity_id, test_case.description);
127+
}
128+
}
129+
}
130+
131+
Ok(())
132+
}
133+
134+
#[test]
135+
fn test_dynamic_edge_cases() -> Result<()> {
136+
// ---
137+
println!("Generating dynamic edge case test data...");
138+
139+
// Generate test data with current timestamps
140+
let test_data = generate_test_data()?;
141+
142+
// Write to temporary file
143+
let temp_file = "testdata/edge-cases-dynamic.json";
144+
fs::write(temp_file, &test_data)?;
145+
146+
println!("Generated test data written to: {}", temp_file);
147+
println!("Test data preview:");
148+
println!("{}", test_data);
149+
println!();
150+
151+
// Run the lambda with our generated data
152+
let results = run_lambda_invoke(temp_file)?;
153+
154+
println!("Lambda returned {} actions", results.len());
155+
156+
// Verify all test expectations
157+
verify_test_expectations(&results)?;
158+
159+
// Additional verification: check expected count
160+
// Should have 5 actions: 6 that should pass - 1 duplicate = 5
161+
// (dedup_first_occurrence appears twice but deduplicated to 1)
162+
let expected_count = 5;
163+
ensure!(
164+
results.len() == expected_count,
165+
"Expected {} actions after filtering and deduplication, got {}",
166+
expected_count,
167+
results.len()
168+
);
169+
170+
// Verify priority sorting (urgent before normal)
171+
let mut seen_normal = false;
172+
for action in &results {
173+
// ---
174+
if action.priority == Priority::Normal {
175+
// ---
176+
seen_normal = true;
177+
} else if action.priority == Priority::Urgent && seen_normal {
178+
// ---
179+
ensure!(false, "Found urgent priority after normal priority - sorting failed");
180+
}
181+
}
182+
183+
// Verify deduplication worked correctly
184+
let duplicate_count =
185+
results.iter().filter(|a| a.entity_id == "dedup_first_occurrence").count();
186+
ensure!(
187+
duplicate_count == 1,
188+
"Expected exactly 1 'duplicate' entity after deduplication, found {}",
189+
duplicate_count
190+
);
191+
192+
// Verify that the duplicate kept the last occurrence (Normal priority)
193+
if let Some(duplicate_action) = results.iter().find(|a| a.entity_id == "duplicate") {
194+
// ---
195+
ensure!(
196+
duplicate_action.priority == Priority::Normal,
197+
"Expected duplicate entity to keep last occurrence (Normal priority), got {:?}",
198+
duplicate_action.priority
199+
);
200+
}
201+
202+
// Cleanup
203+
fs::remove_file(temp_file).ok();
204+
205+
println!();
206+
println!("✅ All dynamic edge case tests passed!");
207+
println!(" - Boundary conditions verified");
208+
println!(" - Deduplication working correctly");
209+
println!(" - Priority sorting maintained");
210+
211+
Ok(())
212+
}

0 commit comments

Comments
 (0)