| name | test-services |
| description | Use when writing or migrating unit tests for the services crate. Covers canonical test patterns, DB fixture setup, mockito HTTP mocking, mockall service mocking, error assertions, concurrency tests, and notification listener tests. Examples: "write tests for a new service method", "migrate old tests to canonical pattern", "add coverage for error paths".
|
services Crate Unit Test Skill
Write and migrate unit tests for the services crate following uniform conventions.
Quick Reference
Every service test follows one of two shapes depending on sync vs async:
Async test (database or HTTP)
#[rstest]
#[tokio::test]
#[anyhow_trace]
async fn test_<service>_<scenario>() -> anyhow::Result<()> {
Ok(())
}
Async test with database fixture
#[rstest]
#[awt]
#[tokio::test]
#[anyhow_trace]
async fn test_<service>_<scenario>(
#[future]
#[from(test_db_service)]
db_service: TestDbService,
) -> anyhow::Result<()> {
let now = db_service.now();
Ok(())
}
Sync test (no async, no database)
#[rstest]
fn test_<service>_<scenario>() -> anyhow::Result<()> {
Ok(())
}
Core Rules
- Annotations (async):
#[rstest] + #[tokio::test] + #[anyhow_trace] on every async test. Add #[awt] ONLY when #[future] fixture params are used.
- Annotations (sync):
#[rstest] only. Do NOT add #[anyhow_trace] on sync tests.
- Naming:
test_<service_name>_<scenario> (e.g. test_db_service_create_download_request)
- Module:
mod tests (not mod test)
- Return: Always
-> anyhow::Result<()> with Ok(()) at end
- Errors: Use
? not .unwrap(). Use .expect("msg") only in non-? contexts (closures, Option chains)
- Assertions:
assert_eq!(expected, actual) with use pretty_assertions::assert_eq;
- Error codes: Assert via
.code() method. Codes are enum_name-variant_name in snake_case.
- Transparent errors: Errors with
#[error(transparent)] delegate to inner error code (e.g., DbError::SqlxError -> "sqlx_error", NOT "db_error-sqlx_error")
- No
use super::*: Use explicit imports in test modules to avoid refactoring issues.
Error Code Convention
Error codes are auto-generated from enum name + variant name in snake_case:
Transparent error delegation: When a variant uses #[error(transparent)]:
pub enum DbError {
#[error(transparent)]
SqlxError(#[from] SqlxError),
}
Assert error codes like:
let err = result.unwrap_err();
assert_eq!("auth_service_error-auth_service_api_error", err.code());
Or use matches! for error variant + field checks:
assert!(matches!(
result.unwrap_err(),
AiApiClientFactoryError::PromptTooLong { max_length: 30, actual_length: 31 }
));
When to Use #[awt]
Use #[awt] ONLY when test parameters use #[future]:
#[rstest]
#[awt]
#[tokio::test]
#[anyhow_trace]
async fn test_something(
#[future]
#[from(test_db_service)]
db_service: TestDbService,
) -> anyhow::Result<()> { ... }
#[rstest]
#[tokio::test]
#[anyhow_trace]
async fn test_something_else() -> anyhow::Result<()> { ... }
Pattern Files
For detailed patterns with full code examples, see:
- db-testing.md -- TestDbService fixture, FrozenTimeService, real SQLite tests
- api-testing.md -- mockito patterns for HTTP service testing
- mock-patterns.md -- mockall setup, MockDbService, expectation-driven tests
- advanced.md -- Concurrency, progress tracking, setting notifications, parameterized tests
Standard Imports
use anyhow_trace::anyhow_trace;
use pretty_assertions::assert_eq;
use rstest::rstest;
use std::sync::Arc;
Additional imports vary by test type:
- DB tests:
use crate::test_utils::{test_db_service, TestDbService};
- Mock tests:
use crate::test_utils::MockDbService; + use mockall::predicate::eq;
- HTTP tests:
use mockito::{Matcher, Server}; + use serde_json::json;
Migration Checklist
When migrating existing tests to the canonical pattern:
When NOT to Use This Skill
- Route handler tests in
routes_app (use the test-routes-app skill instead)
- Integration tests in
routes_all
- Frontend/UI tests (use the
playwright skill instead)
- Tests in
objs crate (different patterns, no service infrastructure)