mirror of https://github.com/buster-so/buster.git
307 lines
13 KiB
Plaintext
307 lines
13 KiB
Plaintext
---
|
|
description: This is designed to help understand how to do testing in this project.
|
|
globs:
|
|
alwaysApply: false
|
|
---
|
|
# Testing Rules and Best Practices
|
|
|
|
## Testing Organization Best Practices
|
|
|
|
### Recommended Mocking Libraries
|
|
|
|
For consistent mocking across the codebase, use these libraries:
|
|
|
|
| Library | Purpose | Use Case |
|
|
|---------|---------|----------|
|
|
| **mockito** | HTTP service mocking | Mocking external API calls in unit tests |
|
|
| **mockall** | Trait/struct mocking | Mocking database or service interfaces in unit tests |
|
|
| **axum-test-helper** / similar | In-memory HTTP requests | Testing server routing/middleware without real HTTP |
|
|
|
|
Always prefer dependency injection patterns to enable easy mocking of dependencies.
|
|
|
|
### Test Utilities and Setup
|
|
|
|
#### Core Database Test Utilities (`libs/database/src/test_utils.rs`)
|
|
|
|
The `database` library provides core utilities for setting up database state consistently across tests that need it (both within `libs/` and in `server/tests/`).
|
|
|
|
```rust
|
|
// Standard TestDb helper from libs/database/src/test_utils.rs
|
|
pub struct TestDb {
|
|
pub test_id: String,
|
|
pub organization_id: Uuid,
|
|
pub user_id: Uuid,
|
|
}
|
|
|
|
impl TestDb {
|
|
/// Creates a new test database environment context.
|
|
/// Note: This does NOT initialize pools, assumes they are already initialized.
|
|
pub async fn new() -> Result<Self> { /* ... */ }
|
|
|
|
/// Get a database connection from the pre-initialized pool.
|
|
pub async fn get_conn(&self) -> Result<PooledConnection<AsyncPgConnection>> { /* ... */ }
|
|
|
|
/// Creates a basic User struct (does not insert).
|
|
pub async fn create_test_user(&self) -> Result<User> { /* ... */ }
|
|
|
|
/// Creates a basic MetricFile struct (does not insert).
|
|
pub async fn create_test_metric_file(&self, owner_id: &Uuid) -> Result<MetricFile> { /* ... */ }
|
|
|
|
/// Creates a basic DashboardFile struct (does not insert).
|
|
pub async fn create_test_dashboard_file(&self, owner_id: &Uuid) -> Result<DashboardFile> { /* ... */ }
|
|
|
|
/// Creates a basic AssetPermission struct (does not insert).
|
|
pub async fn create_asset_permission(
|
|
&self,
|
|
asset_id: &Uuid,
|
|
asset_type: AssetType,
|
|
identity_id: &Uuid,
|
|
role: AssetPermissionRole,
|
|
) -> Result<AssetPermission> { /* ... */ }
|
|
}
|
|
|
|
// Helper functions for inserting test data (using pre-initialized pools)
|
|
|
|
/// Insert a test asset permission.
|
|
pub async fn insert_test_permission(permission: &AssetPermission) -> Result<()> { /* ... */ }
|
|
|
|
/// Insert a test metric file.
|
|
pub async fn insert_test_metric_file(metric_file: &MetricFile) -> Result<()> { /* ... */ }
|
|
|
|
/// Insert a test dashboard file.
|
|
pub async fn insert_test_dashboard_file(dashboard_file: &DashboardFile) -> Result<()> { /* ... */ }
|
|
|
|
/// Clean up test data (assets and permissions) by asset IDs.
|
|
pub async fn cleanup_test_data(asset_ids: &[Uuid]) -> Result<()> { /* ... */ }
|
|
```
|
|
|
|
**Note:** These helpers assume database pools (`pg_pool`, `sqlx_pool`, `redis_pool`) are initialized *before* tests run, typically via a mechanism like `#[ctor]` or a test runner setup.
|
|
|
|
### Library Code Testing Structure
|
|
- **Unit Tests**: Include unit tests inside library source files using `#[cfg(test)]` modules. **Unit tests must mock all external dependencies (DB, HTTP) and never connect to real services.**
|
|
- **Integration Tests**: Place integration tests in the lib's `tests/` directory (e.g., `libs/database/tests/`, `libs/handlers/tests/`). These tests *can* interact with external services (like the test database) using the test utilities.
|
|
- **Test Utils**: Library-specific helpers can reside in `src/test_utils.rs` (for internal use) or `tests/common/mod.rs` (for integration tests within that library).
|
|
|
|
### Server (`server/`) Testing Structure
|
|
- **Location**: API server tests reside in `server/tests/`.
|
|
- **Goal**: These tests focus *only* on the server's responsibilities: routing, middleware application (auth, CORS, logging), request parsing, and response formatting at the framework level. **They should NOT test handler business logic.**
|
|
- **Strategy**: Use Axum's in-memory testing capabilities.
|
|
- **Test Utilities (`server/tests/common/mod.rs`)**: Create server-specific helpers here:
|
|
- `build_test_app()`: Constructs the Axum `Router`, wiring up routes and middleware.
|
|
- **Handler Mocking**: Replace real handlers (from `libs/handlers`) with mock functions that have the *same signature* but return simple `StatusCode` responses (e.g., `StatusCode::OK`, `StatusCode::CREATED`).
|
|
- **Mock Authentication**: Implement a test-only authentication extractor (`MockAuth`) that bypasses real token validation.
|
|
- **Mock State**: If necessary, provide a minimal `MockAppState`.
|
|
- **Testing Tool**: Use `axum_test_helper::TestClient` (or similar) to send requests directly to the in-memory `Router`.
|
|
- **Database Interaction**: If a middleware being tested *requires* specific database state (e.g., checking organization membership), use the standard `database::test_utils` helpers (`TestDb`, `insert_...`, `cleanup_test_data`) to set up that state.
|
|
|
|
### Testing Configuration
|
|
- Use cargo features to make running specific test groups easier (e.g., `cargo test --features integration-tests`).
|
|
- Consider using `cargo-nextest` for parallel test execution.
|
|
|
|
### Running Tests in Different Scopes
|
|
|
|
- **Run all tests**: `cargo test` or `cargo nextest run`
|
|
- **Run library-specific tests**: `cargo test -p <library-name>`
|
|
- **Run server tests**: `cargo test -p server`
|
|
- **Run a specific test**: `cargo test <test_name>`
|
|
- **Run tests with specific pattern**: `cargo test -- <pattern>`
|
|
|
|
### Examples of Proper Test Organization
|
|
|
|
#### Example: Library Unit Test (Mocking DB)
|
|
```rust
|
|
// File: libs/handlers/src/some_handler.rs
|
|
use crate::db_trait::DbConnection; // Using a trait for DI
|
|
use mockall::mock;
|
|
|
|
async fn handle_request(conn: &impl DbConnection, item_id: Uuid) -> Result<Item> {
|
|
conn.find_item(item_id).await
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use mockall::predicate::*;
|
|
|
|
// Define the trait
|
|
#[async_trait::async_trait]
|
|
trait DbConnection {
|
|
async fn find_item(&self, id: Uuid) -> Result<Item>;
|
|
}
|
|
|
|
// Create the mock
|
|
mock! {
|
|
MyDbConnection {} // Name of the mock struct
|
|
#[async_trait::async_trait]
|
|
impl DbConnection for MyDbConnection { // Implement the trait
|
|
async fn find_item(&self, id: Uuid) -> Result<Item>;
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_handle_request_success() -> Result<()> {
|
|
let item_id = Uuid::new_v4();
|
|
let expected_item = Item { id: item_id, name: "Test".to_string() };
|
|
|
|
let mut mock_conn = MockMyDbConnection::new();
|
|
mock_conn.expect_find_item()
|
|
.with(eq(item_id))
|
|
.times(1)
|
|
.returning(move |_| Ok(expected_item.clone()));
|
|
|
|
let result = handle_request(&mock_conn, item_id).await?;
|
|
|
|
assert_eq!(result, expected_item);
|
|
Ok(())
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Example: Library Integration Test (Using `TestDb`)
|
|
```rust
|
|
// File: libs/handlers/tests/metrics/delete_metric_handler_test.rs
|
|
use database::test_utils::{TestDb, insert_test_metric_file, cleanup_test_data};
|
|
use database::pool::get_pg_pool; // To verify deletion
|
|
use database::schema::metric_files;
|
|
use diesel::prelude::*;
|
|
use diesel_async::RunQueryDsl;
|
|
use handlers::metrics::delete_metric_handler; // The real handler
|
|
use anyhow::Result;
|
|
use uuid::Uuid;
|
|
|
|
#[tokio::test]
|
|
async fn test_delete_metric_integration() -> Result<()> {
|
|
// Setup test data using database utilities
|
|
let test_db = TestDb::new().await?;
|
|
let owner_id = test_db.user_id;
|
|
let metric_file = test_db.create_test_metric_file(&owner_id).await?;
|
|
let metric_id = metric_file.id;
|
|
insert_test_metric_file(&metric_file).await?;
|
|
|
|
// Call the actual handler function
|
|
let result = delete_metric_handler(metric_id, &owner_id).await?;
|
|
|
|
// Verify the handler's effect (e.g., soft delete)
|
|
assert!(result.deleted_at.is_some());
|
|
|
|
// Optional: Verify directly in DB
|
|
let mut conn = get_pg_pool().get().await?;
|
|
let db_metric = metric_files::table
|
|
.filter(metric_files::id.eq(metric_id))
|
|
.select(metric_files::deleted_at)
|
|
.first::<Option<chrono::DateTime<chrono::Utc>>>(&mut conn)
|
|
.await?;
|
|
assert!(db_metric.is_some());
|
|
|
|
// Clean up test data using the helper
|
|
cleanup_test_data(&[metric_id]).await?;
|
|
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
#### Example: Server Integration Test (Mocking Handler)
|
|
```rust
|
|
// File: server/tests/routing_test.rs
|
|
mod common; // Assuming common/mod.rs contains build_test_app and mocks
|
|
|
|
use axum::http::StatusCode;
|
|
use axum_test_helper::TestClient;
|
|
use common::build_test_app;
|
|
use uuid::Uuid;
|
|
|
|
#[tokio::test]
|
|
async fn test_get_item_route_exists() {
|
|
let app = build_test_app(); // Builds router with MOCK handlers
|
|
let client = TestClient::new(app);
|
|
let item_id = Uuid::new_v4();
|
|
|
|
// Send request to the endpoint
|
|
let response = client.get(&format!("/items/{}", item_id)).send().await;
|
|
|
|
// Assert that the route exists and the MOCK handler returned OK
|
|
// We are NOT testing the real handler's logic here.
|
|
assert_eq!(response.status(), StatusCode::OK);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_create_item_auth_middleware() {
|
|
let app = build_test_app(); // Assumes app uses a mock auth middleware
|
|
let client = TestClient::new(app);
|
|
|
|
// Send request - mock auth should allow it
|
|
let response = client
|
|
.post("/items")
|
|
.json(&serde_json::json!({ "name": "Test" }))
|
|
.send()
|
|
.await;
|
|
|
|
// Assert the MOCK handler's success code (e.g., CREATED)
|
|
assert_eq!(response.status(), StatusCode::CREATED);
|
|
|
|
// How to test *failure* depends on the mock auth implementation.
|
|
// It might involve sending a specific header to trigger failure in the mock.
|
|
}
|
|
```
|
|
|
|
## General Testing Guidelines
|
|
- All tests must be async and use `#[tokio::test]`.
|
|
- Tests should be well-documented.
|
|
- Each test should focus on a single piece of functionality.
|
|
- Tests should be independent.
|
|
- Use meaningful test names.
|
|
- **Design for testability**: Favor dependency injection, separate logic from IO, use traits.
|
|
|
|
## Unit Tests
|
|
- **Location**: Inline with code using `#[cfg(test)]`.
|
|
- **Scope**: Test individual functions/modules in isolation.
|
|
- **External Dependencies**: MUST be mocked (Database, HTTP APIs, file system, time). Use `mockall` for traits/structs, `mockito` (async) for HTTP.
|
|
- **Database**: Never connect to a real DB. Use trait-based mocks if DB interaction is needed.
|
|
- **HTTP**: Use `mockito::Server::new_async().await`.
|
|
|
|
## Integration Tests
|
|
|
|
### Library Integration Tests (`libs/*/tests/`)
|
|
- **Location**: In each library's `tests/` directory.
|
|
- **Scope**: Test the public API of the library, including its interaction with external services relevant to that library (e.g., database tests interact with the DB).
|
|
- **Database**: Use `database::test_utils` (`TestDb`, `insert_...`, `cleanup_test_data`) to manage test data in a real test database.
|
|
- **External APIs**: Can use `mockito` if testing library functions that call external services.
|
|
- **Isolation**: Tests should ideally not call functions from *other* libraries directly, focus on the current library's integration.
|
|
|
|
### Server Integration Tests (`server/tests/`)
|
|
- **Location**: `server/tests/`.
|
|
- **Scope**: Test the assembled Axum application: routing, middleware, request/response cycle.
|
|
- **Handlers**: Mocked (as described above) - do *not* test handler logic here.
|
|
- **Database**: Only interact with the database via `database::test_utils` if *required* for testing middleware behavior (e.g., setting up a user for auth middleware).
|
|
- **Testing Tool**: `axum_test_helper::TestClient` or similar in-memory tester.
|
|
|
|
### Environment and Configuration
|
|
- Use `.env.test` for test-specific environment variables (Database URLs, API keys).
|
|
- Load config via `dotenv`. The `database` test setup might rely on environment variables being loaded beforehand.
|
|
- Ensure `DATABASE_URL` points to a dedicated test database.
|
|
|
|
### Database Setup for Integration Tests
|
|
- **Initialization**: Assumes connection pools are initialized globally before tests start (e.g., using `#[ctor]` in `libs/database/src/lib.rs` or `tests/mod.rs`).
|
|
- **Test Data Management**: ALWAYS use `database::test_utils`:
|
|
```rust
|
|
#[tokio::test]
|
|
async fn test_something_with_db() -> Result<()> {
|
|
// Assumes pools are initialized
|
|
let test_db = TestDb::new().await?;
|
|
let user = test_db.create_test_user().await?;
|
|
let dashboard = test_db.create_test_dashboard_file(&user.id).await?;
|
|
insert_test_dashboard_file(&dashboard).await?;
|
|
let dashboard_id = dashboard.id;
|
|
|
|
// ... perform test actions ...
|
|
|
|
// Clean up the specific data created for this test
|
|
cleanup_test_data(&[dashboard_id]).await?;
|
|
Ok(())
|
|
}
|
|
```
|
|
- **Schema/Models**: Use the standard definitions from `database::schema` and `database::models`.
|
|
- **Isolation**: `TestDb` provides context, but actual data isolation relies on careful use of `cleanup_test_data` at the end of each test.
|
|
|
|
**IMPORTANT**: Integration tests should use the shared connection pools initialized once for the test run. Tests should *never* create their own pools manually.
|