Initial commit
This commit is contained in:
330
skills/mock-strategy-guide/SKILL.md
Normal file
330
skills/mock-strategy-guide/SKILL.md
Normal file
@@ -0,0 +1,330 @@
|
||||
---
|
||||
name: mock-strategy-guide
|
||||
description: Guides users on creating mock implementations for testing with traits, providing test doubles, and avoiding tight coupling to test infrastructure. Activates when users need to test code with external dependencies.
|
||||
allowed-tools: Read, Grep
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Mock Strategy Guide Skill
|
||||
|
||||
You are an expert at testing strategies for Rust, especially creating mock implementations for hexagonal architecture. When you detect testing needs for code with dependencies, proactively suggest mocking strategies.
|
||||
|
||||
## When to Activate
|
||||
|
||||
Activate when you notice:
|
||||
- Code with external dependencies (DB, HTTP, etc.)
|
||||
- Trait-based abstractions for repositories or services
|
||||
- Tests that require real infrastructure
|
||||
- Questions about mocking or test doubles
|
||||
|
||||
## Mock Implementation Patterns
|
||||
|
||||
### Pattern 1: Simple Mock Repository
|
||||
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
struct MockUserRepository {
|
||||
users: HashMap<String, User>,
|
||||
}
|
||||
|
||||
impl MockUserRepository {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
users: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn with_user(mut self, user: User) -> Self {
|
||||
self.users.insert(user.id.clone(), user);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl UserRepository for MockUserRepository {
|
||||
async fn find(&self, id: &str) -> Result<User, Error> {
|
||||
self.users
|
||||
.get(id)
|
||||
.cloned()
|
||||
.ok_or(Error::NotFound)
|
||||
}
|
||||
|
||||
async fn save(&self, user: &User) -> Result<(), Error> {
|
||||
// Mock just succeeds
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_user_service() {
|
||||
// Arrange
|
||||
let user = User { id: "1".to_string(), email: "test@example.com".to_string() };
|
||||
let mock_repo = MockUserRepository::new().with_user(user.clone());
|
||||
let service = UserService::new(mock_repo);
|
||||
|
||||
// Act
|
||||
let result = service.get_user("1").await;
|
||||
|
||||
// Assert
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap().id, "1");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 2: Mock with Verification
|
||||
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
struct MockEmailService {
|
||||
sent_emails: Arc<Mutex<Vec<Email>>>,
|
||||
}
|
||||
|
||||
impl MockEmailService {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
sent_emails: Arc::new(Mutex::new(Vec::new())),
|
||||
}
|
||||
}
|
||||
|
||||
fn emails_sent(&self) -> Vec<Email> {
|
||||
self.sent_emails.lock().unwrap().clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl EmailService for MockEmailService {
|
||||
async fn send(&self, email: Email) -> Result<(), Error> {
|
||||
self.sent_emails.lock().unwrap().push(email);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_sends_welcome_email() {
|
||||
let mock_email = MockEmailService::new();
|
||||
let service = UserService::new(mock_email.clone());
|
||||
|
||||
service.register_user("test@example.com").await.unwrap();
|
||||
|
||||
// Verify email was sent
|
||||
let emails = mock_email.emails_sent();
|
||||
assert_eq!(emails.len(), 1);
|
||||
assert_eq!(emails[0].to, "test@example.com");
|
||||
assert!(emails[0].subject.contains("Welcome"));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 3: Mock with Controlled Failures
|
||||
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
enum MockBehavior {
|
||||
Success,
|
||||
NotFound,
|
||||
DatabaseError,
|
||||
}
|
||||
|
||||
struct MockRepository {
|
||||
behavior: MockBehavior,
|
||||
}
|
||||
|
||||
impl MockRepository {
|
||||
fn with_behavior(behavior: MockBehavior) -> Self {
|
||||
Self { behavior }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl UserRepository for MockRepository {
|
||||
async fn find(&self, id: &str) -> Result<User, Error> {
|
||||
match self.behavior {
|
||||
MockBehavior::Success => Ok(test_user()),
|
||||
MockBehavior::NotFound => Err(Error::NotFound),
|
||||
MockBehavior::DatabaseError => Err(Error::Database("Connection failed".into())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handles_not_found() {
|
||||
let mock = MockRepository::with_behavior(MockBehavior::NotFound);
|
||||
let service = UserService::new(mock);
|
||||
|
||||
let result = service.get_user("1").await;
|
||||
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(result.unwrap_err(), Error::NotFound));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handles_database_error() {
|
||||
let mock = MockRepository::with_behavior(MockBehavior::DatabaseError);
|
||||
let service = UserService::new(mock);
|
||||
|
||||
let result = service.get_user("1").await;
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 4: Builder Pattern for Mocks
|
||||
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
struct MockRepositoryBuilder {
|
||||
users: HashMap<String, User>,
|
||||
find_error: Option<Error>,
|
||||
save_error: Option<Error>,
|
||||
}
|
||||
|
||||
impl MockRepositoryBuilder {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
users: HashMap::new(),
|
||||
find_error: None,
|
||||
save_error: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn with_user(mut self, user: User) -> Self {
|
||||
self.users.insert(user.id.clone(), user);
|
||||
self
|
||||
}
|
||||
|
||||
fn with_find_error(mut self, error: Error) -> Self {
|
||||
self.find_error = Some(error);
|
||||
self
|
||||
}
|
||||
|
||||
fn build(self) -> MockRepository {
|
||||
MockRepository {
|
||||
users: self.users,
|
||||
find_error: self.find_error,
|
||||
save_error: self.save_error,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_with_builder() {
|
||||
let mock = MockRepositoryBuilder::new()
|
||||
.with_user(test_user())
|
||||
.with_save_error(Error::Database("Save failed".into()))
|
||||
.build();
|
||||
|
||||
let service = UserService::new(mock);
|
||||
|
||||
// Can find user
|
||||
let user = service.get_user("1").await.unwrap();
|
||||
|
||||
// But save fails
|
||||
let result = service.update_user(user).await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## In-Memory Test Implementations
|
||||
|
||||
For integration tests with real logic but no infrastructure:
|
||||
|
||||
```rust
|
||||
pub struct InMemoryUserRepository {
|
||||
users: Arc<Mutex<HashMap<String, User>>>,
|
||||
}
|
||||
|
||||
impl InMemoryUserRepository {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
users: Arc::new(Mutex::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl UserRepository for InMemoryUserRepository {
|
||||
async fn find(&self, id: &str) -> Result<User, Error> {
|
||||
self.users
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get(id)
|
||||
.cloned()
|
||||
.ok_or(Error::NotFound)
|
||||
}
|
||||
|
||||
async fn save(&self, user: &User) -> Result<(), Error> {
|
||||
self.users
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(user.id.clone(), user.clone());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete(&self, id: &str) -> Result<(), Error> {
|
||||
self.users
|
||||
.lock()
|
||||
.unwrap()
|
||||
.remove(id)
|
||||
.ok_or(Error::NotFound)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Test Fixture Helpers
|
||||
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod fixtures {
|
||||
use super::*;
|
||||
|
||||
pub fn test_user() -> User {
|
||||
User {
|
||||
id: "test-id".to_string(),
|
||||
email: "test@example.com".to_string(),
|
||||
name: "Test User".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn test_user_with_id(id: &str) -> User {
|
||||
User {
|
||||
id: id.to_string(),
|
||||
email: "test@example.com".to_string(),
|
||||
name: "Test User".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn test_users(count: usize) -> Vec<User> {
|
||||
(0..count)
|
||||
.map(|i| test_user_with_id(&format!("user-{}", i)))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Your Approach
|
||||
|
||||
When you see code needing tests:
|
||||
1. Identify external dependencies (traits)
|
||||
2. Suggest mock implementation structure
|
||||
3. Show verification patterns
|
||||
4. Provide test fixture helpers
|
||||
|
||||
When you see tests without mocks:
|
||||
1. Suggest extracting trait if tightly coupled
|
||||
2. Show how to create mock implementations
|
||||
3. Demonstrate verification patterns
|
||||
|
||||
Proactively suggest mocking strategies for testable, maintainable code.
|
||||
294
skills/property-testing-guide/SKILL.md
Normal file
294
skills/property-testing-guide/SKILL.md
Normal file
@@ -0,0 +1,294 @@
|
||||
---
|
||||
name: property-testing-guide
|
||||
description: Introduces property-based testing with proptest, helping users find edge cases automatically by testing invariants and properties. Activates when users test algorithms or data structures.
|
||||
allowed-tools: Read, Grep
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Property-Based Testing Guide Skill
|
||||
|
||||
You are an expert at property-based testing in Rust using proptest. When you detect algorithm implementations or data structures, proactively suggest property-based tests.
|
||||
|
||||
## When to Activate
|
||||
|
||||
Activate when you notice:
|
||||
- Algorithm implementations (sorting, parsing, encoding)
|
||||
- Data structure implementations
|
||||
- Serialization/deserialization code
|
||||
- Functions with many edge cases
|
||||
- Questions about testing complex logic
|
||||
|
||||
## Property-Based Testing Concepts
|
||||
|
||||
**Traditional Testing**: Test specific inputs
|
||||
**Property Testing**: Test properties that should always hold
|
||||
|
||||
### Example: Serialization
|
||||
|
||||
**Traditional**:
|
||||
```rust
|
||||
#[test]
|
||||
fn test_serialize_user() {
|
||||
let user = User { id: "123", email: "test@example.com" };
|
||||
let json = serialize(user);
|
||||
assert_eq!(json, r#"{"id":"123","email":"test@example.com"}"#);
|
||||
}
|
||||
```
|
||||
|
||||
**Property-Based**:
|
||||
```rust
|
||||
proptest! {
|
||||
#[test]
|
||||
fn test_serialization_roundtrip(id in "[a-z0-9]+", email in "[a-z]+@[a-z]+\\.com") {
|
||||
let user = User { id, email: email.clone() };
|
||||
let serialized = serialize(&user)?;
|
||||
let deserialized = deserialize(&serialized)?;
|
||||
|
||||
// Property: roundtrip should preserve data
|
||||
prop_assert_eq!(user.id, deserialized.id);
|
||||
prop_assert_eq!(user.email, deserialized.email);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Common Properties to Test
|
||||
|
||||
### 1. Roundtrip Properties
|
||||
|
||||
**Pattern**:
|
||||
```rust
|
||||
use proptest::prelude::*;
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn test_encode_decode_roundtrip(data in ".*") {
|
||||
let encoded = encode(&data);
|
||||
let decoded = decode(&encoded)?;
|
||||
|
||||
// Property: encoding then decoding gives original
|
||||
prop_assert_eq!(data, decoded);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Idempotence
|
||||
|
||||
**Pattern**:
|
||||
```rust
|
||||
proptest! {
|
||||
#[test]
|
||||
fn test_normalize_idempotent(s in ".*") {
|
||||
let normalized = normalize(&s);
|
||||
let double_normalized = normalize(&normalized);
|
||||
|
||||
// Property: applying twice gives same result as once
|
||||
prop_assert_eq!(normalized, double_normalized);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Invariants
|
||||
|
||||
**Pattern**:
|
||||
```rust
|
||||
proptest! {
|
||||
#[test]
|
||||
fn test_sort_invariants(mut vec in prop::collection::vec(any::<i32>(), 0..100)) {
|
||||
let original_len = vec.len();
|
||||
sort(&mut vec);
|
||||
|
||||
// Property 1: Length unchanged
|
||||
prop_assert_eq!(vec.len(), original_len);
|
||||
|
||||
// Property 2: Sorted order
|
||||
for i in 1..vec.len() {
|
||||
prop_assert!(vec[i-1] <= vec[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Comparison with Oracle
|
||||
|
||||
**Pattern**:
|
||||
```rust
|
||||
proptest! {
|
||||
#[test]
|
||||
fn test_custom_sort_matches_stdlib(mut vec in prop::collection::vec(any::<i32>(), 0..100)) {
|
||||
let mut expected = vec.clone();
|
||||
expected.sort();
|
||||
|
||||
custom_sort(&mut vec);
|
||||
|
||||
// Property: matches standard library behavior
|
||||
prop_assert_eq!(vec, expected);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Inverse Functions
|
||||
|
||||
**Pattern**:
|
||||
```rust
|
||||
proptest! {
|
||||
#[test]
|
||||
fn test_add_subtract_inverse(a in any::<i32>(), b in any::<i32>()) {
|
||||
if let Some(sum) = a.checked_add(b) {
|
||||
let result = sum.checked_sub(b);
|
||||
|
||||
// Property: subtraction is inverse of addition
|
||||
prop_assert_eq!(result, Some(a));
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Strategies
|
||||
|
||||
### Strategy for Domain Types
|
||||
|
||||
```rust
|
||||
use proptest::prelude::*;
|
||||
|
||||
fn user_strategy() -> impl Strategy<Value = User> {
|
||||
("[a-z]{5,10}", "[a-z]{3,8}@[a-z]{3,8}\\.com", 18..100u8)
|
||||
.prop_map(|(name, email, age)| User {
|
||||
name,
|
||||
email,
|
||||
age,
|
||||
})
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn test_user_validation(user in user_strategy()) {
|
||||
// Property: all generated users should be valid
|
||||
prop_assert!(validate_user(&user).is_ok());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Strategy with Constraints
|
||||
|
||||
```rust
|
||||
fn positive_money() -> impl Strategy<Value = Money> {
|
||||
(1..1_000_000u64).prop_map(|cents| Money::from_cents(cents))
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn test_money_operations(a in positive_money(), b in positive_money()) {
|
||||
let sum = a + b;
|
||||
|
||||
// Property: sum is greater than both operands
|
||||
prop_assert!(sum >= a);
|
||||
prop_assert!(sum >= b);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Patterns
|
||||
|
||||
### Pattern 1: Parser Testing
|
||||
|
||||
```rust
|
||||
proptest! {
|
||||
#[test]
|
||||
fn test_parser_never_panics(s in ".*") {
|
||||
// Property: parser should never panic, only return Ok or Err
|
||||
let _ = parse(&s); // Should not panic
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_valid_input_parses(
|
||||
name in "[a-zA-Z]+",
|
||||
age in 0..150u8,
|
||||
) {
|
||||
let input = format!("{},{}", name, age);
|
||||
let result = parse(&input);
|
||||
|
||||
// Property: valid input always succeeds
|
||||
prop_assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 2: Data Structure Invariants
|
||||
|
||||
```rust
|
||||
proptest! {
|
||||
#[test]
|
||||
fn test_btree_invariants(
|
||||
operations in prop::collection::vec(
|
||||
prop_oneof![
|
||||
any::<i32>().prop_map(Operation::Insert),
|
||||
any::<i32>().prop_map(Operation::Remove),
|
||||
],
|
||||
0..100
|
||||
)
|
||||
) {
|
||||
let mut tree = BTree::new();
|
||||
|
||||
for op in operations {
|
||||
match op {
|
||||
Operation::Insert(val) => tree.insert(val),
|
||||
Operation::Remove(val) => tree.remove(val),
|
||||
}
|
||||
|
||||
// Property: tree maintains balance invariant
|
||||
prop_assert!(tree.is_balanced());
|
||||
// Property: tree maintains order invariant
|
||||
prop_assert!(tree.is_sorted());
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 3: Equivalence Testing
|
||||
|
||||
```rust
|
||||
proptest! {
|
||||
#[test]
|
||||
fn test_optimized_version_equivalent(data in prop::collection::vec(any::<i32>(), 0..100)) {
|
||||
let result1 = slow_but_correct(&data);
|
||||
let result2 = fast_optimized(&data);
|
||||
|
||||
// Property: optimized version gives same results
|
||||
prop_assert_eq!(result1, result2);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
```toml
|
||||
[dev-dependencies]
|
||||
proptest = "1.0"
|
||||
```
|
||||
|
||||
## Shrinking
|
||||
|
||||
Proptest automatically finds minimal failing cases:
|
||||
|
||||
```rust
|
||||
proptest! {
|
||||
#[test]
|
||||
fn test_divide(a in any::<i32>(), b in any::<i32>()) {
|
||||
let result = divide(a, b); // Fails when b == 0
|
||||
|
||||
// proptest will shrink to smallest failing case: b = 0
|
||||
prop_assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Your Approach
|
||||
|
||||
When you see:
|
||||
1. **Serialization** → Suggest roundtrip property
|
||||
2. **Sorting/ordering** → Suggest invariant properties
|
||||
3. **Parsers** → Suggest "never panics" property
|
||||
4. **Algorithms** → Suggest comparison with oracle
|
||||
5. **Data structures** → Suggest invariant testing
|
||||
|
||||
Proactively suggest property-based tests to find edge cases automatically.
|
||||
336
skills/test-coverage-advisor/SKILL.md
Normal file
336
skills/test-coverage-advisor/SKILL.md
Normal file
@@ -0,0 +1,336 @@
|
||||
---
|
||||
name: test-coverage-advisor
|
||||
description: Reviews test coverage and suggests missing test cases for error paths, edge cases, and business logic. Activates when users write tests or implement new features.
|
||||
allowed-tools: Read, Grep, Glob
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Test Coverage Advisor Skill
|
||||
|
||||
You are an expert at comprehensive test coverage in Rust. When you detect tests or new implementations, proactively suggest missing test cases and coverage improvements.
|
||||
|
||||
## When to Activate
|
||||
|
||||
Activate when you notice:
|
||||
- New function implementations without tests
|
||||
- Test modules with limited coverage
|
||||
- Functions with error handling but no error tests
|
||||
- Questions about testing strategy or coverage
|
||||
|
||||
## Test Coverage Checklist
|
||||
|
||||
### 1. Success Path Testing
|
||||
|
||||
**What to Look For**: Missing happy path tests
|
||||
|
||||
**Pattern**:
|
||||
```rust
|
||||
#[test]
|
||||
fn test_create_user_success() {
|
||||
let user = User::new("test@example.com".to_string(), 25).unwrap();
|
||||
assert_eq!(user.email(), "test@example.com");
|
||||
assert_eq!(user.age(), 25);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Error Path Testing
|
||||
|
||||
**What to Look For**: Functions returning Result but no error tests
|
||||
|
||||
**Missing Tests**:
|
||||
```rust
|
||||
pub fn validate_email(email: &str) -> Result<(), ValidationError> {
|
||||
if email.is_empty() {
|
||||
return Err(ValidationError::Empty);
|
||||
}
|
||||
if !email.contains('@') {
|
||||
return Err(ValidationError::InvalidFormat);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ❌ NO TESTS for error cases!
|
||||
```
|
||||
|
||||
**Suggested Tests**:
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_validate_email_success() {
|
||||
assert!(validate_email("test@example.com").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_email_empty() {
|
||||
let result = validate_email("");
|
||||
assert!(matches!(result, Err(ValidationError::Empty)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_email_missing_at_sign() {
|
||||
let result = validate_email("invalid");
|
||||
assert!(matches!(result, Err(ValidationError::InvalidFormat)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_email_no_domain() {
|
||||
let result = validate_email("test@");
|
||||
assert!(matches!(result, Err(ValidationError::InvalidFormat)));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Suggestion Template**:
|
||||
```
|
||||
Your function returns Result but I don't see tests for error cases. Consider adding:
|
||||
|
||||
#[test]
|
||||
fn test_empty_input() {
|
||||
let result = function("");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_format() {
|
||||
let result = function("invalid");
|
||||
assert!(matches!(result, Err(SpecificError)));
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Edge Cases
|
||||
|
||||
**What to Look For**: Missing boundary tests
|
||||
|
||||
**Common Edge Cases**:
|
||||
- Empty collections
|
||||
- Single item collections
|
||||
- Maximum/minimum values
|
||||
- Null/None values
|
||||
- Zero values
|
||||
- Negative numbers
|
||||
|
||||
**Pattern**:
|
||||
```rust
|
||||
#[test]
|
||||
fn test_empty_list() {
|
||||
let result = process_items(vec![]);
|
||||
assert!(result.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_item() {
|
||||
let result = process_items(vec![item]);
|
||||
assert_eq!(result.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_max_size() {
|
||||
let items = vec![item; 1000];
|
||||
let result = process_items(items);
|
||||
assert!(result.len() <= 1000);
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Async Function Testing
|
||||
|
||||
**What to Look For**: Async functions without async tests
|
||||
|
||||
**Pattern**:
|
||||
```rust
|
||||
#[tokio::test]
|
||||
async fn test_fetch_user_success() {
|
||||
let repo = setup_test_repo().await;
|
||||
let user = repo.find_user("123").await.unwrap();
|
||||
assert_eq!(user.id(), "123");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_fetch_user_not_found() {
|
||||
let repo = setup_test_repo().await;
|
||||
let result = repo.find_user("nonexistent").await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Table-Driven Tests
|
||||
|
||||
**What to Look For**: Multiple similar test cases
|
||||
|
||||
**Before (Repetitive)**:
|
||||
```rust
|
||||
#[test]
|
||||
fn test_valid_email1() {
|
||||
assert!(validate_email("test@example.com").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_valid_email2() {
|
||||
assert!(validate_email("user@domain.org").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_email1() {
|
||||
assert!(validate_email("invalid").is_err());
|
||||
}
|
||||
```
|
||||
|
||||
**After (Table-Driven)**:
|
||||
```rust
|
||||
#[test]
|
||||
fn test_email_validation() {
|
||||
let test_cases = vec![
|
||||
("test@example.com", true, "Valid email"),
|
||||
("user@domain.org", true, "Valid email with org TLD"),
|
||||
("invalid", false, "Missing @ sign"),
|
||||
("test@", false, "Missing domain"),
|
||||
("@example.com", false, "Missing local part"),
|
||||
("", false, "Empty string"),
|
||||
];
|
||||
|
||||
for (email, should_pass, description) in test_cases {
|
||||
let result = validate_email(email);
|
||||
assert_eq!(
|
||||
result.is_ok(),
|
||||
should_pass,
|
||||
"Failed for {}: {}",
|
||||
email,
|
||||
description
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Anti-Patterns
|
||||
|
||||
### ❌ Testing Implementation Details
|
||||
|
||||
```rust
|
||||
// BAD: Testing private fields
|
||||
#[test]
|
||||
fn test_internal_state() {
|
||||
let obj = MyStruct::new();
|
||||
assert_eq!(obj.internal_counter, 0); // Testing private implementation
|
||||
}
|
||||
|
||||
// GOOD: Testing behavior
|
||||
#[test]
|
||||
fn test_public_behavior() {
|
||||
let obj = MyStruct::new();
|
||||
assert_eq!(obj.get_count(), 0); // Testing public interface
|
||||
}
|
||||
```
|
||||
|
||||
### ❌ Tests Without Assertions
|
||||
|
||||
```rust
|
||||
// BAD: No assertion
|
||||
#[test]
|
||||
fn test_function() {
|
||||
function(); // What are we testing?
|
||||
}
|
||||
|
||||
// GOOD: Clear assertion
|
||||
#[test]
|
||||
fn test_function() {
|
||||
let result = function();
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
```
|
||||
|
||||
### ❌ Overly Complex Tests
|
||||
|
||||
```rust
|
||||
// BAD: Test does too much
|
||||
#[test]
|
||||
fn test_everything() {
|
||||
// 100 lines of setup
|
||||
// Multiple operations
|
||||
// Many assertions
|
||||
}
|
||||
|
||||
// GOOD: Focused tests
|
||||
#[test]
|
||||
fn test_create() { /* ... */ }
|
||||
|
||||
#[test]
|
||||
fn test_update() { /* ... */ }
|
||||
|
||||
#[test]
|
||||
fn test_delete() { /* ... */ }
|
||||
```
|
||||
|
||||
## Coverage Tools
|
||||
|
||||
```bash
|
||||
# Using tarpaulin
|
||||
cargo install cargo-tarpaulin
|
||||
cargo tarpaulin --out Html
|
||||
|
||||
# Using llvm-cov
|
||||
cargo install cargo-llvm-cov
|
||||
cargo llvm-cov --html
|
||||
cargo llvm-cov --open # Open in browser
|
||||
```
|
||||
|
||||
## Test Organization
|
||||
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// Helper functions
|
||||
fn setup() -> TestData {
|
||||
TestData::new()
|
||||
}
|
||||
|
||||
// Success cases
|
||||
mod success {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_valid_input() { /* ... */ }
|
||||
}
|
||||
|
||||
// Error cases
|
||||
mod errors {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_invalid_input() { /* ... */ }
|
||||
|
||||
#[test]
|
||||
fn test_missing_data() { /* ... */ }
|
||||
}
|
||||
|
||||
// Edge cases
|
||||
mod edge_cases {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_empty_input() { /* ... */ }
|
||||
|
||||
#[test]
|
||||
fn test_max_size() { /* ... */ }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Your Approach
|
||||
|
||||
When you see implementations:
|
||||
1. Check for test module
|
||||
2. Identify untested error paths
|
||||
3. Look for missing edge cases
|
||||
4. Suggest specific test cases with code
|
||||
|
||||
When you see tests:
|
||||
1. Check coverage of error paths
|
||||
2. Suggest table-driven tests for similar cases
|
||||
3. Point out missing edge cases
|
||||
4. Recommend organization improvements
|
||||
|
||||
Proactively suggest missing tests to improve robustness.
|
||||
Reference in New Issue
Block a user