Initial commit
This commit is contained in:
436
commands/rust-test-add-integration.md
Normal file
436
commands/rust-test-add-integration.md
Normal file
@@ -0,0 +1,436 @@
|
||||
---
|
||||
description: Create integration tests in the tests/ directory
|
||||
---
|
||||
|
||||
You are helping create integration tests for a Rust project in the tests/ directory.
|
||||
|
||||
## Your Task
|
||||
|
||||
Set up integration test infrastructure and create comprehensive integration tests that test the public API.
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Ask for Test Details**
|
||||
|
||||
Ask the user:
|
||||
- What feature/module to integration test?
|
||||
- Do you need database/HTTP mocking?
|
||||
- Should we use testcontainers for real databases?
|
||||
|
||||
2. **Create Integration Test File**
|
||||
|
||||
Create a new file in `tests/` directory:
|
||||
|
||||
```
|
||||
tests/
|
||||
├── [feature]_integration.rs
|
||||
└── common/
|
||||
└── mod.rs # Shared test utilities
|
||||
```
|
||||
|
||||
3. **Set Up Common Utilities**
|
||||
|
||||
Create `tests/common/mod.rs`:
|
||||
|
||||
```rust
|
||||
// tests/common/mod.rs
|
||||
#![allow(dead_code)]
|
||||
|
||||
use my_crate::*;
|
||||
|
||||
pub fn setup() {
|
||||
// Common setup logic
|
||||
}
|
||||
|
||||
pub fn teardown() {
|
||||
// Common cleanup logic
|
||||
}
|
||||
```
|
||||
|
||||
4. **Generate Integration Test**
|
||||
|
||||
Create the integration test file:
|
||||
|
||||
```rust
|
||||
// tests/[feature]_integration.rs
|
||||
use my_crate::*;
|
||||
|
||||
mod common;
|
||||
|
||||
#[test]
|
||||
fn test_[feature]_complete_workflow() {
|
||||
common::setup();
|
||||
|
||||
// Arrange
|
||||
let app = create_test_application();
|
||||
|
||||
// Act
|
||||
let result = app.execute_workflow();
|
||||
|
||||
// Assert
|
||||
assert!(result.is_ok());
|
||||
|
||||
common::teardown();
|
||||
}
|
||||
|
||||
fn create_test_application() -> Application {
|
||||
// Setup test instance with test configuration
|
||||
Application::new(test_config())
|
||||
}
|
||||
|
||||
fn test_config() -> Config {
|
||||
Config {
|
||||
database_url: "postgres://localhost/test_db".to_string(),
|
||||
// ... other test config
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
5. **Add Async Integration Tests**
|
||||
|
||||
For async applications:
|
||||
|
||||
```rust
|
||||
#[tokio::test]
|
||||
async fn test_async_integration() {
|
||||
let app = setup_test_app().await;
|
||||
|
||||
let result = app.process_request(test_request()).await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
async fn setup_test_app() -> Application {
|
||||
Application::new(test_config()).await.unwrap()
|
||||
}
|
||||
```
|
||||
|
||||
6. **Add Database Integration Tests**
|
||||
|
||||
Using testcontainers:
|
||||
|
||||
```rust
|
||||
use testcontainers::{clients, images::postgres::Postgres};
|
||||
use sqlx::PgPool;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_with_real_database() {
|
||||
let docker = clients::Cli::default();
|
||||
let postgres = docker.run(Postgres::default());
|
||||
|
||||
let connection_string = format!(
|
||||
"postgres://postgres@localhost:{}/postgres",
|
||||
postgres.get_host_port_ipv4(5432)
|
||||
);
|
||||
|
||||
let pool = PgPool::connect(&connection_string).await.unwrap();
|
||||
|
||||
// Run migrations
|
||||
sqlx::migrate!("./migrations")
|
||||
.run(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Now test with real database
|
||||
let repo = PostgresRepository::new(pool);
|
||||
let service = MyService::new(repo);
|
||||
|
||||
let result = service.create_item("test").await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
```
|
||||
|
||||
Or create a helper in common:
|
||||
|
||||
```rust
|
||||
// tests/common/mod.rs
|
||||
use sqlx::PgPool;
|
||||
use testcontainers::*;
|
||||
|
||||
pub async fn setup_test_database() -> PgPool {
|
||||
let docker = clients::Cli::default();
|
||||
let postgres = docker.run(images::postgres::Postgres::default());
|
||||
|
||||
let url = format!(
|
||||
"postgres://postgres@localhost:{}/postgres",
|
||||
postgres.get_host_port_ipv4(5432)
|
||||
);
|
||||
|
||||
let pool = PgPool::connect(&url).await.unwrap();
|
||||
sqlx::migrate!().run(&pool).await.unwrap();
|
||||
|
||||
pool
|
||||
}
|
||||
|
||||
// tests/database_integration.rs
|
||||
mod common;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_repository() {
|
||||
let pool = common::setup_test_database().await;
|
||||
let repo = MyRepository::new(pool);
|
||||
|
||||
// Test repository operations
|
||||
}
|
||||
```
|
||||
|
||||
7. **Add HTTP API Integration Tests**
|
||||
|
||||
For testing HTTP APIs:
|
||||
|
||||
```rust
|
||||
use axum::body::Body;
|
||||
use axum::http::{Request, StatusCode};
|
||||
use tower::ServiceExt;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_api_endpoint() {
|
||||
let app = create_test_app();
|
||||
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/api/users")
|
||||
.method("GET")
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
|
||||
let body = hyper::body::to_bytes(response.into_body())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let users: Vec<User> = serde_json::from_slice(&body).unwrap();
|
||||
assert!(!users.is_empty());
|
||||
}
|
||||
```
|
||||
|
||||
Or using reqwest for full HTTP testing:
|
||||
|
||||
```rust
|
||||
#[tokio::test]
|
||||
async fn test_api_full_request() {
|
||||
let server = spawn_test_server().await;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let response = client
|
||||
.get(format!("{}/api/users", server.url()))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(response.status(), 200);
|
||||
|
||||
let users: Vec<User> = response.json().await.unwrap();
|
||||
assert!(!users.is_empty());
|
||||
}
|
||||
|
||||
async fn spawn_test_server() -> TestServer {
|
||||
// Start server on random port for testing
|
||||
TestServer::spawn().await
|
||||
}
|
||||
```
|
||||
|
||||
8. **Add HTTP Mocking with wiremock**
|
||||
|
||||
For testing external API calls:
|
||||
|
||||
```rust
|
||||
use wiremock::{MockServer, Mock, ResponseTemplate};
|
||||
use wiremock::matchers::{method, path};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_external_api_integration() {
|
||||
let mock_server = MockServer::start().await;
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/external/api"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(
|
||||
serde_json::json!({
|
||||
"status": "ok",
|
||||
"data": "test"
|
||||
})
|
||||
))
|
||||
.mount(&mock_server)
|
||||
.await;
|
||||
|
||||
let client = ExternalApiClient::new(&mock_server.uri());
|
||||
let result = client.fetch_data().await.unwrap();
|
||||
|
||||
assert_eq!(result.status, "ok");
|
||||
}
|
||||
```
|
||||
|
||||
9. **Add Multi-Step Workflow Tests**
|
||||
|
||||
Test complete user workflows:
|
||||
|
||||
```rust
|
||||
#[tokio::test]
|
||||
async fn test_complete_user_workflow() {
|
||||
let app = setup_test_app().await;
|
||||
|
||||
// Step 1: Create user
|
||||
let user_id = app
|
||||
.create_user("test@example.com")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Step 2: Retrieve user
|
||||
let user = app
|
||||
.get_user(&user_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(user.email, "test@example.com");
|
||||
|
||||
// Step 3: Update user
|
||||
app.update_user(&user_id, "new@example.com")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Step 4: Verify update
|
||||
let updated = app
|
||||
.get_user(&user_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(updated.email, "new@example.com");
|
||||
|
||||
// Step 5: Delete user
|
||||
app.delete_user(&user_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Step 6: Verify deletion
|
||||
let result = app.get_user(&user_id).await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
```
|
||||
|
||||
10. **Update Dev Dependencies**
|
||||
|
||||
Ensure required dependencies are in Cargo.toml:
|
||||
|
||||
```toml
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1", features = ["full", "test-util"] }
|
||||
testcontainers = "0.15"
|
||||
wiremock = "0.6"
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
```
|
||||
|
||||
11. **Provide Summary**
|
||||
|
||||
```
|
||||
✅ Integration tests created successfully!
|
||||
|
||||
## Files Created:
|
||||
- `tests/[feature]_integration.rs` - Main integration test file
|
||||
- `tests/common/mod.rs` - Shared test utilities
|
||||
|
||||
## Tests Added:
|
||||
- test_[feature]_complete_workflow
|
||||
- test_database_operations
|
||||
- test_api_endpoints
|
||||
- test_external_api_integration
|
||||
|
||||
## Infrastructure:
|
||||
- Database test setup with testcontainers
|
||||
- HTTP mocking with wiremock
|
||||
- Test configuration helpers
|
||||
|
||||
## Dependencies Added:
|
||||
[List of dev dependencies]
|
||||
|
||||
## Running Integration Tests:
|
||||
|
||||
```bash
|
||||
# Run all integration tests
|
||||
cargo test --test [feature]_integration
|
||||
|
||||
# Run specific test
|
||||
cargo test test_complete_workflow
|
||||
|
||||
# Run with output
|
||||
cargo test --test [feature]_integration -- --nocapture
|
||||
|
||||
# Run all integration tests
|
||||
cargo test --tests
|
||||
```
|
||||
|
||||
## Next Steps:
|
||||
1. Review and customize test cases
|
||||
2. Add more workflow scenarios
|
||||
3. Run tests: `cargo test --tests`
|
||||
4. Check if you need more test infrastructure
|
||||
```
|
||||
|
||||
## Integration Test Patterns
|
||||
|
||||
### Setup/Teardown Pattern
|
||||
```rust
|
||||
struct TestContext {
|
||||
pool: PgPool,
|
||||
// Other resources
|
||||
}
|
||||
|
||||
impl TestContext {
|
||||
async fn new() -> Self {
|
||||
// Setup
|
||||
Self {
|
||||
pool: setup_database().await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TestContext {
|
||||
fn drop(&mut self) {
|
||||
// Cleanup
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_with_context() {
|
||||
let ctx = TestContext::new().await;
|
||||
// Use ctx.pool
|
||||
}
|
||||
```
|
||||
|
||||
### Parallel Test Isolation
|
||||
```rust
|
||||
#[tokio::test]
|
||||
async fn test_isolated_1() {
|
||||
let db = create_unique_test_db("test1").await;
|
||||
// Each test gets its own database
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_isolated_2() {
|
||||
let db = create_unique_test_db("test2").await;
|
||||
// Runs in parallel without interference
|
||||
}
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
- Integration tests are in separate crates from src/
|
||||
- Each file in tests/ is a separate binary
|
||||
- Common code goes in tests/common/ (not tests/common.rs)
|
||||
- Use real implementations when possible
|
||||
- Mock external services
|
||||
- Clean up resources after tests
|
||||
- Tests should be independent and runnable in any order
|
||||
|
||||
## After Completion
|
||||
|
||||
Ask the user:
|
||||
1. Did the integration tests pass?
|
||||
2. Do you need more test scenarios?
|
||||
3. Should we add performance/load tests?
|
||||
4. Do you want to set up CI/CD for these tests?
|
||||
337
commands/rust-test-add-unit.md
Normal file
337
commands/rust-test-add-unit.md
Normal file
@@ -0,0 +1,337 @@
|
||||
---
|
||||
description: Add comprehensive unit tests for a function or module
|
||||
---
|
||||
|
||||
You are helping add unit tests to Rust code following best practices.
|
||||
|
||||
## Your Task
|
||||
|
||||
Generate comprehensive unit tests for the specified function or module, covering success cases, error cases, and edge cases.
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Identify Target**
|
||||
|
||||
Ask the user (if not specified):
|
||||
- What function/struct/module to test?
|
||||
- Where is it located?
|
||||
|
||||
Or scan the current file for testable functions.
|
||||
|
||||
2. **Analyze Function**
|
||||
|
||||
Read the function to understand:
|
||||
- What it does
|
||||
- What inputs it takes
|
||||
- What it returns (including error types)
|
||||
- What edge cases exist
|
||||
|
||||
3. **Create Test Module**
|
||||
|
||||
Add or update the `#[cfg(test)]` module at the bottom of the file:
|
||||
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// Tests go here
|
||||
}
|
||||
```
|
||||
|
||||
4. **Generate Test Cases**
|
||||
|
||||
For each function, create tests for:
|
||||
|
||||
**Success Cases**:
|
||||
```rust
|
||||
#[test]
|
||||
fn test_[function]_with_valid_input() {
|
||||
// Arrange
|
||||
let input = create_valid_input();
|
||||
|
||||
// Act
|
||||
let result = function(input);
|
||||
|
||||
// Assert
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), expected_value);
|
||||
}
|
||||
```
|
||||
|
||||
**Error Cases**:
|
||||
```rust
|
||||
#[test]
|
||||
fn test_[function]_returns_error_on_invalid_input() {
|
||||
let result = function(invalid_input);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(result.unwrap_err(), ErrorType::Specific));
|
||||
}
|
||||
```
|
||||
|
||||
**Edge Cases**:
|
||||
```rust
|
||||
#[test]
|
||||
fn test_[function]_with_empty_input() {
|
||||
let result = function("");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_[function]_with_max_length_input() {
|
||||
let long_input = "x".repeat(MAX_LENGTH);
|
||||
let result = function(&long_input);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
```
|
||||
|
||||
5. **Add Test Fixtures**
|
||||
|
||||
Create helper functions for common test data:
|
||||
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn create_test_user() -> User {
|
||||
User {
|
||||
id: "test-id".to_string(),
|
||||
email: "test@example.com".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn create_test_user_with_id(id: &str) -> User {
|
||||
User {
|
||||
id: id.to_string(),
|
||||
email: "test@example.com".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_with_fixture() {
|
||||
let user = create_test_user();
|
||||
let result = validate_user(&user);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
6. **Add Async Tests if Needed**
|
||||
|
||||
For async functions:
|
||||
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_async_function() {
|
||||
let result = async_function().await;
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_async_error_case() {
|
||||
let result = async_function_with_error().await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
7. **Add Mock Implementations**
|
||||
|
||||
For functions using traits:
|
||||
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
struct MockRepository {
|
||||
data: HashMap<String, User>,
|
||||
}
|
||||
|
||||
impl MockRepository {
|
||||
fn new() -> Self {
|
||||
Self { data: HashMap::new() }
|
||||
}
|
||||
|
||||
fn with_user(mut self, user: User) -> Self {
|
||||
self.data.insert(user.id.clone(), user);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl UserRepository for MockRepository {
|
||||
async fn find_by_id(&self, id: &str) -> Result<User, Error> {
|
||||
self.data.get(id)
|
||||
.cloned()
|
||||
.ok_or(Error::NotFound(id.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_service_with_mock() {
|
||||
let mock = MockRepository::new()
|
||||
.with_user(create_test_user());
|
||||
|
||||
let service = UserService::new(mock);
|
||||
let user = service.get_user("test-id").await.unwrap();
|
||||
|
||||
assert_eq!(user.email, "test@example.com");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
8. **Add Table-Driven Tests**
|
||||
|
||||
For multiple similar test cases:
|
||||
|
||||
```rust
|
||||
#[test]
|
||||
fn test_validation_cases() {
|
||||
let test_cases = vec![
|
||||
("", false, "Empty input"),
|
||||
("a", true, "Single char"),
|
||||
("abc", true, "Valid input"),
|
||||
("x".repeat(1000).as_str(), false, "Too long"),
|
||||
];
|
||||
|
||||
for (input, should_pass, description) in test_cases {
|
||||
let result = validate(input);
|
||||
assert_eq!(
|
||||
result.is_ok(),
|
||||
should_pass,
|
||||
"Failed for case: {}",
|
||||
description
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
9. **Provide Test Summary**
|
||||
|
||||
After generating tests:
|
||||
|
||||
```
|
||||
✅ Unit tests added successfully!
|
||||
|
||||
## Tests Created:
|
||||
|
||||
### Success Cases (3):
|
||||
- test_create_user_with_valid_email
|
||||
- test_update_user_success
|
||||
- test_delete_user_success
|
||||
|
||||
### Error Cases (4):
|
||||
- test_create_user_with_empty_email
|
||||
- test_create_user_with_invalid_email
|
||||
- test_update_nonexistent_user
|
||||
- test_delete_nonexistent_user
|
||||
|
||||
### Edge Cases (2):
|
||||
- test_email_max_length
|
||||
- test_special_characters_in_email
|
||||
|
||||
## Test Fixtures:
|
||||
- create_test_user()
|
||||
- create_test_user_with_email(email)
|
||||
|
||||
## Mock Implementations:
|
||||
- MockUserRepository
|
||||
|
||||
## Run Tests:
|
||||
```bash
|
||||
cargo test
|
||||
cargo test --package [package_name]
|
||||
cargo test test_create_user -- --nocapture
|
||||
```
|
||||
|
||||
## Next Steps:
|
||||
1. Review generated tests
|
||||
2. Add more edge cases if needed
|
||||
3. Run tests and verify they pass
|
||||
4. Check coverage with cargo tarpaulin
|
||||
```
|
||||
|
||||
## Test Naming Conventions
|
||||
|
||||
Use descriptive names following this pattern:
|
||||
- `test_[function]_[scenario]_[expected_result]`
|
||||
|
||||
Examples:
|
||||
- `test_create_user_with_valid_email_succeeds`
|
||||
- `test_create_user_with_empty_email_returns_validation_error`
|
||||
- `test_parse_config_with_invalid_json_returns_parse_error`
|
||||
|
||||
## Important Patterns
|
||||
|
||||
### Testing Result Types
|
||||
```rust
|
||||
#[test]
|
||||
fn test_returns_ok() {
|
||||
let result = function();
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_returns_specific_error() {
|
||||
let result = function();
|
||||
assert!(result.is_err());
|
||||
|
||||
match result {
|
||||
Err(MyError::Specific) => (), // Expected
|
||||
_ => panic!("Wrong error type"),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Testing Option Types
|
||||
```rust
|
||||
#[test]
|
||||
fn test_returns_some() {
|
||||
let result = function();
|
||||
assert!(result.is_some());
|
||||
assert_eq!(result.unwrap(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_returns_none() {
|
||||
let result = function();
|
||||
assert!(result.is_none());
|
||||
}
|
||||
```
|
||||
|
||||
### Testing Panics
|
||||
```rust
|
||||
#[test]
|
||||
#[should_panic(expected = "expected panic message")]
|
||||
fn test_panics_on_invalid_input() {
|
||||
function_that_panics();
|
||||
}
|
||||
```
|
||||
|
||||
### Testing with assert_matches
|
||||
```rust
|
||||
use assert_matches::assert_matches;
|
||||
|
||||
#[test]
|
||||
fn test_error_variant() {
|
||||
let result = function();
|
||||
assert_matches!(result, Err(Error::Specific { .. }));
|
||||
}
|
||||
```
|
||||
|
||||
## After Completion
|
||||
|
||||
Ask the user:
|
||||
1. Do tests pass? (run `cargo test`)
|
||||
2. Are there more scenarios to test?
|
||||
3. Should we add integration tests?
|
||||
4. Do you want to check test coverage?
|
||||
Reference in New Issue
Block a user