MANDATORY: All tests that create databases or interact with database state MUST use #[serial] annotation.
use serial_test::serial;
#[test]
#[serial] // Required for database tests
fn test_database_feature() {
let handle = create_database(config).expect("Failed to create database");
// test implementation
}
#[test]
// No #[serial] needed - no database interaction
fn test_pure_logic() {
assert_eq!(2 + 2, 4);
}When to use #[serial]:
- Tests that call
create_database() - Tests that interact with database handles
- Tests that modify shared registry state
- Tests that access database files
When NOT needed:
- Pure unit tests with no database
- Tests that only check module existence
- Tests that validate types/interfaces
- Tests with no shared state
MANDATORY: Every CREATE TABLE statement MUST be preceded by DROP TABLE IF EXISTS.
// [x] CORRECT
execute(handle, "DROP TABLE IF EXISTS users".to_string()).ok();
execute(handle, "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)".to_string())
.expect("Failed to create table");
// [ ] WRONG - Will cause test interference
execute(handle, "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)".to_string())
.expect("Failed to create table");Why:
- Tests may run on databases that weren't properly cleaned up
- Parallel test execution can leave tables in unexpected states
- Ensures idempotent test behavior
- Prevents "table already exists" errors
MANDATORY: All test databases MUST use thread ID for uniqueness.
let thread_id = std::thread::current().id();
let config = DatabaseConfig {
name: format!("uniffi_test_name_{:?}.db", thread_id),
encryption_key: None,
};Why: Ensures each test has its own isolated database even if tests somehow run in parallel.
MANDATORY: Every test that creates a database MUST close it.
let handle = create_database(config).expect("Failed to create database");
// ... test operations ...
close_database(handle).expect("Failed to close database");Why: Prevents handle leakage and ensures proper cleanup of resources.
MANDATORY: Every test that creates a database MUST delete the file after closing it.
let thread_id = std::thread::current().id();
let config = DatabaseConfig {
name: format!("uniffi_test_{:?}.db", thread_id),
encryption_key: None,
};
let handle = create_database(config).expect("Failed to create database");
// ... test operations ...
close_database(handle).expect("Failed to close database");
// Cleanup: delete test database file
let db_path = format!("uniffi_test_{:?}.db", thread_id);
let _ = std::fs::remove_file(&db_path);Why: Prevents accumulation of test database files in the repository.
Use this template for all new UniFFI tests:
#[test]
#[serial]
fn test_my_feature() {
let _ = env_logger::builder().is_test(true).try_init();
let thread_id = std::thread::current().id();
let config = DatabaseConfig {
name: format!("uniffi_myfeature_{:?}.db", thread_id),
encryption_key: None,
};
let handle = create_database(config).expect("Failed to create database");
// DROP before CREATE - ALWAYS
execute(handle, "DROP TABLE IF EXISTS my_table".to_string()).ok();
execute(handle, "CREATE TABLE my_table (id INTEGER PRIMARY KEY)".to_string())
.expect("Failed to create table");
// Test operations here
// Always close
close_database(handle).expect("Failed to close database");
// Cleanup: delete test database file
let db_path = format!("uniffi_myfeature_{:?}.db", thread_id);
let _ = std::fs::remove_file(&db_path);
}#[test] // WRONG - no #[serial]
fn test_something() {
// Will cause race conditions
}// WRONG - no DROP TABLE IF EXISTS
execute(handle, "CREATE TABLE test (id INTEGER)".to_string())
.expect("Failed to create table");// WRONG - all tests use same database
let config = DatabaseConfig {
name: "test.db".to_string(), // No thread ID
encryption_key: None,
};let handle = create_database(config).expect("Failed to create database");
// ... test operations ...
// WRONG - missing close_database(handle)let handle = create_database(config).expect("Failed to create database");
// ... test operations ...
close_database(handle).expect("Failed to close database");
// WRONG - missing std::fs::remove_file cleanupFor batch tests, apply the same pattern to each table:
let statements = vec![
"DROP TABLE IF EXISTS users".to_string(),
"CREATE TABLE users (id INTEGER PRIMARY KEY)".to_string(),
"INSERT INTO users (id) VALUES (1)".to_string(),
];
execute_batch(handle, statements).expect("Batch should succeed");- Tests must be isolated and independent
- No shared state between tests
- Proper cleanup ensures deterministic behavior
- Serial execution prevents race conditions
- Tests must pass 100% of the time
- No "it works in isolation" excuses
- Proper isolation/cleanup is mandatory
- Race conditions are unacceptable
Before submitting a test, verify:
- Uses
#[serial]annotation - Uses thread-based unique database name
- Every CREATE TABLE has DROP TABLE IF EXISTS before it
- Database handle is closed at end of test
- Database file is deleted after closing (std::fs::remove_file)
- Test passes when run alone
- Test passes when run with all other tests
- Test passes 3+ times in a row without failures
- No .db files remain after running tests
See these files for correct patterns:
src/__tests__/uniffi_execute_test.rssrc/__tests__/uniffi_execute_params_test.rssrc/__tests__/uniffi_transactions_test.rssrc/__tests__/uniffi_export_import_test.rssrc/__tests__/uniffi_batch_test.rs
The Golden Rules:
#[serial]on every database testDROP TABLE IF EXISTSbefore everyCREATE TABLE- Thread ID in every database name
- Close every database handle
- Delete every database file after closing
Follow these rules religiously. No exceptions.