Skip to main content
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);
}
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:
Test: "creates a user"
  at src/actions/user_action.rs:25

  expect!(actual).to_equal(expected)

  Expected: "[email protected]"
  Received: "[email protected]"

Testing Approaches (Traditional)

Kit also provides traditional ways to write database-enabled tests: 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]:
  1. Services Bootstrapped: All services marked with #[injectable] are automatically registered, so App::resolve::<T>() works just like in production
  2. Fresh Database: A new in-memory SQLite database is created for each test
  3. Migrations Applied: Your crate::migrations::Migrator runs automatically
  4. 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
  5. 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