Documentation Index
Fetch the complete documentation index at: https://docs.kit-rs.dev/llms.txt
Use this file to discover all available pages before exploring further.
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