Kit provides testing utilities that make it easy to write tests for your actions, services, and controllers with full database support using in-memory SQLite. Kit also offers Jest-like testing macros for better test organization and clearer failure output.
Quick Start
The simplest way to write a test with database support using Jest-like syntax:
use kit::{describe, test, expect};
use kit::testing::TestDatabase;
describe!("UserService", {
test!("creates a user successfully", async fn(db: TestDatabase) {
let action = App::resolve::<CreateUserAction>().unwrap();
let user = action.execute("[email protected]").await.unwrap();
expect!(user.id).to_be_greater_than(0);
expect!(user.email).to_equal("[email protected]".to_string());
});
});
Or using the traditional attribute macro:
use kit::kit_test;
use kit::testing::TestDatabase;
#[kit_test]
async fn test_user_creation(db: TestDatabase) {
let action = CreateUserAction::new();
let user = action.execute("[email protected]").await.unwrap();
assert!(user.id > 0);
}
Jest-like Testing (Recommended)
Kit provides Jest-like macros for better test organization and clearer assertion output.
The describe! Macro
Group related tests with descriptive names:
use kit::{describe, test, expect};
use kit::testing::TestDatabase;
describe!("ListTodosAction", {
test!("returns empty list when no todos exist", async fn(db: TestDatabase) {
let action = App::resolve::<ListTodosAction>().unwrap();
let todos = action.execute().await.unwrap();
expect!(todos).to_be_empty();
});
test!("returns all todos", async fn(db: TestDatabase) {
// Create test data
Todo::create().title("Test Todo".to_string()).save().await.unwrap();
let action = App::resolve::<ListTodosAction>().unwrap();
let todos = action.execute().await.unwrap();
expect!(todos).to_have_length(1);
});
// Nested describe blocks for sub-groups
describe!("with pagination", {
test!("returns first page", async fn(db: TestDatabase) {
// ...
});
});
});
The test! Macro
Define individual test cases with three syntax options:
// Async test with database
test!("creates a user", async fn(db: TestDatabase) {
let action = App::resolve::<CreateUserAction>().unwrap();
let user = action.execute("[email protected]").await.unwrap();
expect!(user.email).to_equal("[email protected]".to_string());
});
// Async test without database
test!("calculates sum", async fn() {
let result = calculate(1, 2).await;
expect!(result).to_equal(3);
});
// Sync test
test!("adds numbers", fn() {
expect!(1 + 1).to_equal(2);
});
The expect! Macro
Fluent assertions with clear failure output:
// Equality
expect!(actual).to_equal(expected);
expect!(actual).to_not_equal(unexpected);
// Boolean
expect!(condition).to_be_true();
expect!(condition).to_be_false();
// Option
expect!(option).to_be_some();
expect!(option).to_be_none();
// Result
expect!(result).to_be_ok();
expect!(result).to_be_err();
// Strings
expect!(string).to_contain("substring");
expect!(string).to_start_with("prefix");
expect!(string).to_end_with("suffix");
expect!(string).to_have_length(10);
expect!(string).to_be_empty();
// Collections
expect!(vec).to_have_length(3);
expect!(vec).to_contain(&item);
expect!(vec).to_be_empty();
// Numeric comparisons
expect!(10).to_be_greater_than(5);
expect!(5).to_be_less_than(10);
expect!(10).to_be_greater_than_or_equal(10);
expect!(5).to_be_less_than_or_equal(5);
Clear Failure Output
When an assertion fails, you get clear output with the test name:
Testing Approaches (Traditional)
Kit also provides traditional ways to write database-enabled tests:
1. Attribute Macro (Recommended)
The #[kit_test] attribute macro is the cleanest way to write tests:
use kit::kit_test;
use kit::testing::TestDatabase;
#[kit_test]
async fn test_create_todo(db: TestDatabase) {
// db is an in-memory SQLite database with all migrations applied
let action = CreateTodoAction::new();
let todo = action.execute("Buy groceries").await.unwrap();
// Query directly using db.conn()
let found = todos::Entity::find_by_id(todo.id)
.one(db.conn())
.await
.unwrap();
assert!(found.is_some());
assert_eq!(found.unwrap().title, "Buy groceries");
}
2. Helper Macro
For more control, use the test_database! macro:
use kit::test_database;
#[tokio::test]
async fn test_todo_list() {
let db = test_database!();
// Create some test data
let action = CreateTodoAction::new();
action.execute("Task 1").await.unwrap();
action.execute("Task 2").await.unwrap();
// Test the list action
let list_action = ListTodosAction::new();
let todos = list_action.execute().await.unwrap();
assert_eq!(todos.len(), 2);
}
How It Works
When you use #[kit_test]:
- Services Bootstrapped: All services marked with
#[injectable] are automatically registered, so App::resolve::<T>() works just like in production
- Fresh Database: A new in-memory SQLite database is created for each test
- Migrations Applied: Your
crate::migrations::Migrator runs automatically
- Automatic Integration: The test database is registered in the DI container, so any code using
DB::connection() or #[inject] db: Database automatically uses the test database
- Complete Isolation: Each test is fully isolated - no data leaks between tests
The #[kit_test] macro calls App::init() and App::boot_services() before your test runs, ensuring all injectable services are available.
Testing Actions
Actions marked with #[injectable] can be resolved from the container in tests:
// Your action
#[injectable]
pub struct CreateUserAction {
#[inject]
db: Database,
}
impl CreateUserAction {
pub async fn execute(&self, email: &str) -> Result<users::Model, FrameworkError> {
let user = users::ActiveModel {
email: Set(email.to_string()),
..Default::default()
};
users::Entity::insert_one(user).await
}
}
// Your test - resolve the action from the container
#[kit_test]
async fn test_create_user(db: TestDatabase) {
// Resolve the action from the DI container
let action = App::resolve::<CreateUserAction>().unwrap();
let user = action.execute("[email protected]").await.unwrap();
// Verify in database
let count = users::Entity::find()
.count(db.conn())
.await
.unwrap();
assert_eq!(count, 1);
}
Custom Migrator
By default, both macros use crate::migrations::Migrator. If your migrator is in a different location:
// With attribute macro
#[kit_test(migrator = my_crate::CustomMigrator)]
async fn test_with_custom_migrator(db: TestDatabase) {
// ...
}
// With helper macro
#[tokio::test]
async fn test_with_custom_migrator() {
let db = test_database!(my_crate::CustomMigrator);
// ...
}
Direct Database Access
The TestDatabase struct provides methods for direct database queries:
#[kit_test]
async fn test_database_queries(db: TestDatabase) {
// Use db.conn() for SeaORM queries
let users = users::Entity::find()
.all(db.conn())
.await
.unwrap();
// Or use db.db() to get the DbConnection
let conn = db.db();
}
Test Without Database Parameter
If you don’t need direct database access in your test but still want the database set up:
#[kit_test]
async fn test_action_indirectly() {
// Database is set up, but we don't need direct access
// Actions using DB::connection() still work
let action = MyAction::new();
let result = action.execute().await.unwrap();
assert!(result.success);
}
Best Practices
1. One Assertion Per Test
Keep tests focused on a single behavior:
#[kit_test]
async fn test_user_email_is_stored(db: TestDatabase) {
let action = CreateUserAction::new();
let user = action.execute("[email protected]").await.unwrap();
assert_eq!(user.email, "[email protected]");
}
#[kit_test]
async fn test_user_gets_default_role(db: TestDatabase) {
let action = CreateUserAction::new();
let user = action.execute("[email protected]").await.unwrap();
assert_eq!(user.role, "user");
}
2. Test Edge Cases
#[kit_test]
async fn test_create_user_with_duplicate_email(db: TestDatabase) {
let action = CreateUserAction::new();
// First user succeeds
action.execute("[email protected]").await.unwrap();
// Second user with same email should fail
let result = action.execute("[email protected]").await;
assert!(result.is_err());
}
3. Use Factories for Test Data
Create helper functions to generate test data:
async fn create_test_user(db: &TestDatabase, email: &str) -> users::Model {
let user = users::ActiveModel {
email: Set(email.to_string()),
..Default::default()
};
user.insert(db.conn()).await.unwrap()
}
#[kit_test]
async fn test_delete_user(db: TestDatabase) {
let user = create_test_user(&db, "[email protected]").await;
let action = DeleteUserAction::new();
action.execute(user.id).await.unwrap();
let found = users::Entity::find_by_id(user.id)
.one(db.conn())
.await
.unwrap();
assert!(found.is_none());
}
Running Tests
Run your tests using cargo:
# Run all tests
cargo test
# Run a specific test
cargo test test_user_creation
# Run tests with output
cargo test -- --nocapture