update testing

This commit is contained in:
dal 2025-04-02 12:46:14 -06:00
parent 1feb7558c2
commit 2abf522c78
No known key found for this signature in database
GPG Key ID: 16F4B0E1E9F61122
7 changed files with 205 additions and 247 deletions

View File

@ -381,60 +381,36 @@ async fn test_user_creation() -> Result<()> {
#### Database Setup
- All integration tests must import and utilize the application's schema from [schema.rs](mdc:src/database/schema.rs)
- Database models from [models.rs](mdc:src/database/models.rs) should be used for test data setup and verification
- Create a robust database test helper:
- Use the `testkit` library for test IDs and directly access pools from the database library:
```rust
// tests/common/db.rs
use anyhow::Result;
use diesel_async::{AsyncPgConnection, AsyncConnection};
use diesel_async::pooled_connection::AsyncDieselConnectionManager;
use deadpool_diesel::postgres::Pool;
use diesel_async::AsyncPgConnection;
use database::pool::get_pg_pool;
use testkit::test_id;
use uuid::Uuid;
pub struct TestDb {
pub pool: Pool,
pub test_id: String, // Unique identifier for this test run
}
impl TestDb {
/// Creates a new isolated test database environment
pub async fn new() -> Result<Self> {
// Initialize environment
crate::common::env::init_test_env();
pub fn new() -> Self {
// Generate unique test identifier
let test_id = Uuid::new_v4().to_string();
let test_id = test_id();
// Get database config from env
let database_url = std::env::var("TEST_DATABASE_URL")
.expect("TEST_DATABASE_URL must be set for tests");
// Create connection manager and pool
let config = AsyncDieselConnectionManager::<AsyncPgConnection>::new(database_url);
let pool = Pool::builder(config)
.max_size(5)
.build()?;
let db = Self { pool, test_id };
// Setup initial test data
db.setup_schema().await?;
Ok(db)
Self { test_id }
}
/// Setup schema and initial test data
async fn setup_schema(&self) -> Result<()> {
let conn = &mut self.pool.get().await?;
// Run migrations or setup tables
// Add any shared setup like creating test users
Ok(())
/// Get a database connection from the pre-initialized pool
pub async fn get_conn(&self) -> Result<diesel_async::pooled_connection::bb8::PooledConnection<AsyncPgConnection>> {
Ok(get_pg_pool().get().await?)
}
/// Clean up test data created by this test instance
pub async fn cleanup(&self) -> Result<()> {
let conn = &mut self.pool.get().await?;
let mut conn = self.get_conn().await?;
// Delete data with test_id marker
// Reset sequences if needed
@ -444,6 +420,8 @@ async fn test_user_creation() -> Result<()> {
/// Create test data with proper isolation using the test_id
pub async fn create_test_data(&self) -> Result<()> {
let mut conn = self.get_conn().await?;
// Create test data tagged with test_id for isolation
Ok(())
}
@ -458,16 +436,21 @@ async fn test_user_creation() -> Result<()> {
```
#### Required Environment Variables
Create a `.env.test` file with necessary test configurations:
The testkit automatically creates a `.env.test` file if one doesn't exist, with necessary default test configurations. You can customize it with:
```env
TEST_DATABASE_URL=postgres://user:pass@localhost/test_db
TEST_POOLER_URL=postgres://user:pass@localhost/test_db
TEST_REDIS_URL=redis://localhost:6379
TEST_DATABASE_POOL_SIZE=10
TEST_SQLX_POOL_SIZE=10
TEST_API_KEY=test-key
TEST_ENV=test
# Optional: enable test logging
# TEST_LOG=debug
```
**IMPORTANT**: All integration tests must have all required services configured and accessible via environment variables loaded through dotenv. Tests should never be written with hardcoded service configurations.
**IMPORTANT**: All integration tests should use the pools from the database library (already initialized by the testkit at build time). Tests should never be written with hardcoded service configurations or manual pool creation.
#### Service Mocks
Create reusable mock services for common external dependencies:
@ -992,29 +975,29 @@ mod tests {
## Example Application Integration Test
```rust
use crate::tests::common::{db::TestDb, env::init_test_env};
use crate::database::{models, schema};
use crate::tests::common::db::TestDb;
use crate::database::{models, schema, pool};
use crate::handlers::users::get_user_handler;
use anyhow::Result;
#[tokio::test]
async fn test_user_creation_flow() -> Result<()> {
// Initialize test environment with proper setup
init_test_env();
// Setup test database with isolation
let test_db = TestDb::new().await?;
// TestDb handles test ID generation and connection access
let test_db = TestDb::new();
// Create test user using fixture or builder pattern
let test_user = models::User::builder()
.email("test@example.com")
.name("Test User")
.test_id(&test_db.test_id) // Set test ID for isolation
.build();
// Insert test data with proper error handling
let mut conn = test_db.get_conn().await?;
diesel::insert_into(schema::users::table)
.values(&test_user)
.execute(&mut test_db.pool.get().await?)
.execute(&mut conn)
.await?;
// Create HTTP client for API testing

View File

@ -381,60 +381,36 @@ async fn test_user_creation() -> Result<()> {
#### Database Setup
- All integration tests must import and utilize the application's schema from [schema.rs](mdc:src/database/schema.rs)
- Database models from [models.rs](mdc:src/database/models.rs) should be used for test data setup and verification
- Create a robust database test helper:
- Use the `testkit` library for test IDs and directly access pools from the database library:
```rust
// tests/common/db.rs
use anyhow::Result;
use diesel_async::{AsyncPgConnection, AsyncConnection};
use diesel_async::pooled_connection::AsyncDieselConnectionManager;
use deadpool_diesel::postgres::Pool;
use diesel_async::AsyncPgConnection;
use database::pool::get_pg_pool;
use testkit::test_id;
use uuid::Uuid;
pub struct TestDb {
pub pool: Pool,
pub test_id: String, // Unique identifier for this test run
}
impl TestDb {
/// Creates a new isolated test database environment
pub async fn new() -> Result<Self> {
// Initialize environment
crate::common::env::init_test_env();
pub fn new() -> Self {
// Generate unique test identifier
let test_id = Uuid::new_v4().to_string();
let test_id = test_id();
// Get database config from env
let database_url = std::env::var("TEST_DATABASE_URL")
.expect("TEST_DATABASE_URL must be set for tests");
// Create connection manager and pool
let config = AsyncDieselConnectionManager::<AsyncPgConnection>::new(database_url);
let pool = Pool::builder(config)
.max_size(5)
.build()?;
let db = Self { pool, test_id };
// Setup initial test data
db.setup_schema().await?;
Ok(db)
Self { test_id }
}
/// Setup schema and initial test data
async fn setup_schema(&self) -> Result<()> {
let conn = &mut self.pool.get().await?;
// Run migrations or setup tables
// Add any shared setup like creating test users
Ok(())
/// Get a database connection from the pre-initialized pool
pub async fn get_conn(&self) -> Result<diesel_async::pooled_connection::bb8::PooledConnection<AsyncPgConnection>> {
Ok(get_pg_pool().get().await?)
}
/// Clean up test data created by this test instance
pub async fn cleanup(&self) -> Result<()> {
let conn = &mut self.pool.get().await?;
let mut conn = self.get_conn().await?;
// Delete data with test_id marker
// Reset sequences if needed
@ -444,6 +420,8 @@ async fn test_user_creation() -> Result<()> {
/// Create test data with proper isolation using the test_id
pub async fn create_test_data(&self) -> Result<()> {
let mut conn = self.get_conn().await?;
// Create test data tagged with test_id for isolation
Ok(())
}
@ -458,16 +436,21 @@ async fn test_user_creation() -> Result<()> {
```
#### Required Environment Variables
Create a `.env.test` file with necessary test configurations:
The testkit automatically creates a `.env.test` file if one doesn't exist, with necessary default test configurations. You can customize it with:
```env
TEST_DATABASE_URL=postgres://user:pass@localhost/test_db
TEST_POOLER_URL=postgres://user:pass@localhost/test_db
TEST_REDIS_URL=redis://localhost:6379
TEST_DATABASE_POOL_SIZE=10
TEST_SQLX_POOL_SIZE=10
TEST_API_KEY=test-key
TEST_ENV=test
# Optional: enable test logging
# TEST_LOG=debug
```
**IMPORTANT**: All integration tests must have all required services configured and accessible via environment variables loaded through dotenv. Tests should never be written with hardcoded service configurations.
**IMPORTANT**: All integration tests should use the pools from the database library (already initialized by the testkit at build time). Tests should never be written with hardcoded service configurations or manual pool creation.
#### Service Mocks
Create reusable mock services for common external dependencies:
@ -992,29 +975,29 @@ mod tests {
## Example Application Integration Test
```rust
use crate::tests::common::{db::TestDb, env::init_test_env};
use crate::database::{models, schema};
use crate::tests::common::db::TestDb;
use crate::database::{models, schema, pool};
use crate::handlers::users::get_user_handler;
use anyhow::Result;
#[tokio::test]
async fn test_user_creation_flow() -> Result<()> {
// Initialize test environment with proper setup
init_test_env();
// Setup test database with isolation
let test_db = TestDb::new().await?;
// TestDb handles test ID generation and connection access
let test_db = TestDb::new();
// Create test user using fixture or builder pattern
let test_user = models::User::builder()
.email("test@example.com")
.name("Test User")
.test_id(&test_db.test_id) // Set test ID for isolation
.build();
// Insert test data with proper error handling
let mut conn = test_db.get_conn().await?;
diesel::insert_into(schema::users::table)
.values(&test_user)
.execute(&mut test_db.pool.get().await?)
.execute(&mut conn)
.await?;
// Create HTTP client for API testing

View File

@ -1,6 +0,0 @@
# Test Environment Configuration
TEST_ENV=test
TEST_DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres
TEST_REDIS_URL=redis://localhost:6379
TEST_LOG=true
TEST_LOG_LEVEL=debug

139
api/testkit/CLAUDE.md Normal file
View File

@ -0,0 +1,139 @@
# Testkit Library Usage Guide
## Overview
The testkit library initializes database pools during build time and provides utilities for test isolation. The pools themselves are accessed directly from the database library.
## Key Features
- Pre-initialized database connections at build time
- Environment variable management for test-specific configurations
- Test ID generation for test isolation
## Usage
### Basic Usage
```rust
use database::pool::get_pg_pool;
use testkit::test_id;
use anyhow::Result;
#[tokio::test]
async fn test_database_operations() -> Result<()> {
// Create a unique test ID for isolation
let test_id = test_id();
// Pools are already initialized during build time
// Get the pool from the database library
let pool = get_pg_pool();
// Use the pool in your test
let conn = pool.get().await?;
// Perform database operations...
Ok(())
}
```
### Environment Configuration
The testkit automatically loads environment variables from `.env.test` if available, otherwise from `.env` during the build process. Key environment variables include:
- `TEST_DATABASE_URL` - PostgreSQL connection string for tests
- `TEST_POOLER_URL` - Connection string for the SQL pooler
- `TEST_REDIS_URL` - Redis connection string
- `TEST_DATABASE_POOL_SIZE` - Maximum connections in Diesel pool (default: 10)
- `TEST_SQLX_POOL_SIZE` - Maximum connections in SQLx pool (default: 10)
### Accessing Database Pools
Access the pre-initialized database pools directly through the database library:
```rust
// Get the Diesel PostgreSQL pool (AsyncPgConnection)
let pg_pool = database::pool::get_pg_pool();
// Get the SQLx PostgreSQL pool
let sqlx_pool = database::pool::get_sqlx_pool();
// Get the Redis pool
let redis_pool = database::pool::get_redis_pool();
```
### Test ID Generation
For test isolation, you can generate unique IDs to tag test data:
```rust
let test_id = testkit::test_id();
// Use this ID to mark test data for cleanup
diesel::insert_into(users::table)
.values((
users::name.eq("Test User"),
users::email.eq("test@example.com"),
users::test_id.eq(test_id), // Use the test ID for later cleanup
))
.execute(&mut conn)
.await?;
```
## Implementation Details
### Pool Initialization
The testkit initializes pools during the build process using `build.rs`. This ensures pools are always available when your tests run, with no initialization overhead or risk of connection timing issues.
### Error Handling
- Build-time initialization failures are reported as warnings but don't fail the build
- Unit tests that don't need database access will run fine even if pool initialization failed
- Integration tests that need database access will fail fast if the database isn't available
### Database Reset (Optional Feature)
For clearing test data, use the `db_reset` feature:
```rust
// Only available when the `db_reset` feature is enabled
#[cfg(feature = "db_reset")]
testkit::reset_test_database().await?;
```
## Best Practices
1. **Direct Pool Access**: Always use `get_pg_pool()`, `get_sqlx_pool()`, or `get_redis_pool()` directly
2. **Use Test IDs**: Generate and use test IDs for proper test isolation
3. **Cleanup After Tests**: Always clean up test data after tests
4. **Connection Pooling**: Reuse connections from the pool instead of creating new ones
5. **Environment Variables**: Use the `.env.test` file for test-specific configuration
6. **Avoid Database in Unit Tests**: Unit tests should mock database operations
## Example Integration Test Pattern
```rust
#[tokio::test]
async fn test_example() -> Result<()> {
// Generate unique test ID
let test_id = testkit::test_id();
// Get pre-initialized database connection directly from database lib
let pool = database::pool::get_pg_pool();
let mut conn = pool.get().await?;
// Setup test data with test_id for tracking
let test_user = create_test_user(&mut conn, &test_id).await?;
// Run the test against the test data
let result = your_function_under_test(test_user.id).await?;
assert_eq!(result.name, test_user.name);
// Clean up test data using test_id
cleanup_test_data(&mut conn, &test_id).await?;
Ok(())
}
async fn create_test_user(conn: &mut PgConnection, test_id: &str) -> Result<User> {
// Insert user with test_id
// ...
}
async fn cleanup_test_data(conn: &mut PgConnection, test_id: &str) -> Result<()> {
// Delete all data with matching test_id
// ...
}
```

View File

@ -1,76 +0,0 @@
# Buster API Test Kit
The `testkit` crate provides standardized database pool initialization for tests across the Buster API workspace, leveraging the existing database pools from the database library.
## Features
- **Automatic Database Pool Configuration**: Uses the database library's pools with test configurations
- **Environment Configuration**: Uses `.env.test` for test database configuration
- **Test ID Generation**: Provides unique IDs for test data isolation
## Getting Started
Add the testkit as a dev-dependency in your workspace crate:
```toml
[dev-dependencies]
testkit = { path = "../../testkit" }
```
## Usage
```rust
use anyhow::Result;
#[tokio::test]
async fn my_test() -> Result<()> {
// Get database pools - these are initialized from the database library
let pg_pool = testkit::get_pg_pool();
let redis_pool = testkit::get_redis_pool();
// Use the pools for testing
let conn = pg_pool.get().await?;
// Generate a unique test ID for data isolation
let test_id = testkit::test_id();
// Use test_id to tag and later clean up test data
Ok(())
}
```
## Test Data Isolation
Use the `test_id()` function to generate unique IDs for isolating test data:
```rust
// Get a unique ID
let test_id = testkit::test_id();
// Tag test data with the ID
diesel::sql_query("INSERT INTO users (name, test_id) VALUES ($1, $2)")
.bind::<diesel::sql_types::Text, _>("Test User")
.bind::<diesel::sql_types::Text, _>(&test_id)
.execute(&mut conn)
.await?;
// Clean up after test
diesel::sql_query("DELETE FROM users WHERE test_id = $1")
.bind::<diesel::sql_types::Text, _>(&test_id)
.execute(&mut conn)
.await?;
```
## Environment Configuration
By default, the testkit checks for and creates a `.env.test` file with:
```
TEST_DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres
TEST_REDIS_URL=redis://localhost:6379
TEST_LOG=true
TEST_LOG_LEVEL=debug
```
When running tests, the testkit automatically sets `DATABASE_URL` to the value of `TEST_DATABASE_URL`, ensuring that tests use the test database.

View File

@ -93,7 +93,8 @@ fn try_init_pools() -> Result<(), String> {
Err(e) => return Err(e.to_string()),
};
// Try to initialize pools but don't fail the build if it fails
// Try to initialize pools through the testkit's internal function
// This won't fail the build if it can't connect to the database
runtime.block_on(async {
match database::pool::init_pools().await {
Ok(_) => Ok(()),

View File

@ -3,13 +3,13 @@ use dotenv::dotenv;
use std::path::Path;
use std::sync::Once;
use once_cell::sync::OnceCell;
use database::pool::{PgPool, PgPoolSqlx, RedisPool};
static ENV_INIT: Once = Once::new();
static POOL_INIT: OnceCell<()> = OnceCell::new();
/// Initialize the test environment by setting up .env.test
pub fn init_test_env() {
/// This should only be used internally or by build.rs
fn init_test_env() {
ENV_INIT.call_once(|| {
// Try loading .env.test first, then fall back to .env
if Path::new(".env.test").exists() {
@ -44,8 +44,10 @@ pub fn init_test_env() {
});
}
/// Initialize database and Redis pools for testing
pub async fn init_pools() -> Result<()> {
// This function is only for internal use by build.rs - the pools should be
// initialized at build time, not during test runtime
#[doc(hidden)]
pub async fn _internal_init_pools() -> Result<()> {
// Setup the environment first
init_test_env();
@ -54,7 +56,7 @@ pub async fn init_pools() -> Result<()> {
return Ok(());
}
// Use the init_test_pools function which is specifically designed for tests
// Use the database crate's init_pools function
let result = match database::pool::init_pools().await {
Ok(_) => {
// Success case - cache the result
@ -72,42 +74,8 @@ pub async fn init_pools() -> Result<()> {
result
}
/// Get the initialized PG pool, initializing it first if needed
pub fn get_pg_pool() -> &'static PgPool {
ensure_pools_initialized();
database::pool::get_pg_pool()
}
/// Get the initialized SQLX pool, initializing it first if needed
pub fn get_sqlx_pool() -> &'static PgPoolSqlx {
ensure_pools_initialized();
database::pool::get_sqlx_pool()
}
/// Get the initialized Redis pool, initializing it first if needed
pub fn get_redis_pool() -> &'static RedisPool {
ensure_pools_initialized();
database::pool::get_redis_pool()
}
/// Helper function to ensure pools are initialized
fn ensure_pools_initialized() {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(async {
// Always initialize test environment
init_test_env();
// If pools aren't initialized yet, try to initialize them
if POOL_INIT.get().is_none() {
if let Err(e) = init_pools().await {
panic!("Failed to initialize database pools for tests: {}", e);
}
}
});
}
// No need for pool accessor functions - users should access pools directly
// through the database library (database::pool::get_pg_pool(), etc.)
/// Generate a unique test ID - useful for creating test resources
pub fn test_id() -> String {
@ -119,38 +87,4 @@ pub async fn cleanup_connection() -> Result<()> {
// This is a placeholder for any future cleanup that might be needed
// Currently the pools handle their own cleanup
Ok(())
}
/// Reset the test database to a clean state
/// Only use this in integration tests where you need a completely fresh DB
/// Most tests should isolate their data instead
#[cfg(feature = "db_reset")]
pub async fn reset_test_database() -> Result<()> {
let pool = get_pg_pool();
let mut conn = pool.get().await?;
// Execute a transaction that truncates all tables
// This code is only included when the db_reset feature is enabled
// as it's potentially destructive
diesel::sql_query("BEGIN").execute(&mut conn).await?;
// List of tables to truncate - add more as needed
let tables = vec![
"users", "organizations", "users_to_organizations",
"api_keys", "teams", "teams_to_users",
"data_sources", "datasets", "dataset_columns",
"permission_groups", "datasets_to_permission_groups",
"terms", "collections", "dashboards", "threads", "messages"
];
for table in tables {
diesel::sql_query(format!("TRUNCATE TABLE {} CASCADE", table))
.execute(&mut conn)
.await?;
}
diesel::sql_query("COMMIT").execute(&mut conn).await?;
Ok(())
}
}