Testing
Learn how to test your Ultimo applications effectively.
Test Client
:::info Coming Soon Built-in test client is coming soon. :::
The planned test client will make integration testing easy:
use ultimo::prelude::*;
use ultimo::test::TestClient;
#[tokio::test]
async fn test_get_users() {
let mut app = Ultimo::new();
app.get("/users", |ctx| async move {
ctx.json(vec![
User { id: 1, name: "Alice".to_string() }
]).await
});
let client = TestClient::new(app);
let response = client.get("/users").send().await?;
assert_eq!(response.status(), 200);
assert_eq!(response.json::<Vec<User>>().await?.len(), 1);
}Testing Patterns
Unit Testing Handlers
Test your handler logic separately:
#[derive(Serialize)]
struct User {
id: u32,
name: String,
}
async fn get_users() -> Vec<User> {
vec![
User { id: 1, name: "Alice".to_string() },
User { id: 2, name: "Bob".to_string() },
]
}
#[tokio::test]
async fn test_get_users() {
let users = get_users().await;
assert_eq!(users.len(), 2);
assert_eq!(users[0].name, "Alice");
}Integration Testing
Test the full HTTP stack:
use reqwest;
#[tokio::test]
async fn test_api_integration() {
// Start your server in the background
tokio::spawn(async {
let mut app = Ultimo::new();
app.get("/health", |ctx| async move {
ctx.json(json!({"status": "ok"})).await
});
app.listen("127.0.0.1:3001").await
});
// Give server time to start
tokio::time::sleep(Duration::from_millis(100)).await;
// Test with real HTTP client
let response = reqwest::get("http://127.0.0.1:3001/health")
.await?;
assert_eq!(response.status(), 200);
}Testing with Database
#[tokio::test]
async fn test_user_crud() {
// Setup test database
let pool = SqlxPool::connect("postgres://localhost/test_db").await?;
let mut app = Ultimo::new();
app.with_sqlx(pool);
// Add routes
app.post("/users", create_user_handler);
app.get("/users/:id", get_user_handler);
let client = TestClient::new(app);
// Test create
let response = client
.post("/users")
.json(&json!({"name": "Alice", "email": "alice@example.com"}))
.send()
.await?;
assert_eq!(response.status(), 201);
let user: User = response.json().await?;
// Test get
let response = client
.get(&format!("/users/{}", user.id))
.send()
.await?;
assert_eq!(response.status(), 200);
}Mocking Dependencies
// Mock RPC client for testing
struct MockRpcClient;
impl MockRpcClient {
async fn get_user(&self, id: u32) -> User {
User {
id,
name: "Test User".to_string(),
email: "test@example.com".to_string(),
}
}
}
#[tokio::test]
async fn test_with_mock() {
let mock_client = MockRpcClient;
let user = mock_client.get_user(1).await;
assert_eq!(user.name, "Test User");
}Best Practices
✅ Use #[tokio::test] for Async Tests
#[tokio::test]
async fn test_async_handler() {
let result = async_operation().await;
assert!(result.is_ok());
}✅ Clean Up Test Data
#[tokio::test]
async fn test_with_cleanup() {
// Setup
let db = setup_test_db().await;
// Test
let result = test_operation(&db).await;
// Cleanup
cleanup_test_db(&db).await;
assert!(result.is_ok());
}✅ Test Error Cases
#[tokio::test]
async fn test_invalid_input() {
let result = create_user(CreateUserInput {
name: "".to_string(), // Invalid
email: "not-an-email".to_string(),
}).await;
assert!(result.is_err());
}✅ Use Test Fixtures
struct TestFixture {
app: Ultimo,
db: SqlxPool,
}
impl TestFixture {
async fn new() -> Self {
let db = SqlxPool::connect("postgres://localhost/test_db").await.unwrap();
let mut app = Ultimo::new();
app.with_sqlx(db.clone());
Self { app, db }
}
}
#[tokio::test]
async fn test_with_fixture() {
let fixture = TestFixture::new().await;
// Use fixture.app and fixture.db
}CI/CD Integration
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:14
env:
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v3
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: Run tests
run: cargo test
env:
DATABASE_URL: postgres://postgres:postgres@localhost/test_dbCoverage
Generate test coverage reports:
# Install tarpaulin
cargo install cargo-tarpaulin
# Run tests with coverage
cargo tarpaulin --out Html --output-dir coverage