Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Testing and benchmarks

Testing a panel

Test a panel over HTTP, in memory: build the router against a test database and send it requests. TestClient carries cookies, a tenant and a CSRF token, with helpers to build form bodies and read responses. An app reaches it as tablo::testing through the facade’s testing feature; enable it for tests only:

[dev-dependencies]
tablo = { version = "0.2.0", features = ["sqlite", "testing"] }
#![allow(unused)]
fn main() {
use tablo::testing::{TestClient, form_body};

#[tokio::test]
async fn books_cannot_be_deleted() {
    let mut db = seeded_db().await; // your fixture: an in-memory database with rows
    let id = first_book_id(&db).await;
    let router = Router::builder()
        .discover()
        .app_context(db.clone())
        .panel(
            Panel::new("admin")
                .resource::<BookResource>()
                .auth(Auth::disabled()),
        )
        .expect("mount the panel")
        .build();
    let client = TestClient::new(&router);

    assert_eq!(client.get("/admin/books").await.status(), 200);

    // A POST needs the CSRF cookie and a matching `csrf_token` field.
    let token = uuid::Uuid::new_v4().to_string();
    let response = client
        .csrf(&token)
        .post_form(
            &format!("/admin/books/{id}/delete"),
            form_body(&[("csrf_token", &token), ("confirm", "1")]),
        )
        .await;
    assert_eq!(response.status(), 403); // the policy does not allow `DeleteAny`

    let books = Book::all().exec(&mut db).await.expect("list books");
    assert_eq!(books.len(), 1); // nothing was deleted
}
}
  • Cover every ability your policy decides with a request it allows and one it refuses, and assert on the database as well as the status code.
  • Cover query() scoping by seeding a row the scope excludes and asserting that the list, the detail page and a delete all miss it.
  • Tenancy. client.tenant(id) scopes a request to a tenant, the way a signed-in user’s tenant would.
  • Signed-in requests. With authentication on, sign in through POST /admin/login once and reuse the client’s cookies, or insert an AuthSession row directly to skip the password hash.
  • Declaration mistakes. A panel that refuses to mount returns a MountError inside the router builder’s error. Downcast to it and match on each mistake’s DeclarationErrorKind rather than on its message:
#![allow(unused)]
fn main() {
#[tokio::test]
async fn the_panel_refuses_a_db_without_the_auth_models() {
    let db = Db::builder()
        .models(toasty::models!(Book))
        .connect("sqlite::memory:")
        .await
        .expect("connect");
    let error = Router::builder()
        .discover()
        .app_context(db)
        .panel(Panel::new("admin").resource::<BookResource>())
        .err()
        .expect("auth is on, and the Db does not register its models");

    let refusal = error
        .downcast_ref::<tablo::MountError>()
        .expect("a declaration mistake");
    assert!(matches!(
        refusal.errors()[0].kind,
        tablo::DeclarationErrorKind::MissingAuthModels { .. }
    ));
}
}

examples/showcase/tests/ is a complete suite covering lists, forms, deletes, filters, export, tenancy, uploads and authentication; its common module holds the fixtures above.

The browser scripts

tablo-ui’s client scripts are plain browser scripts with no build step. Their unit tests run on Node’s built-in runner:

node --test crates/tablo-ui/assets/*.test.js

Benchmarks

The benchmark renders a 50-row list with two included relations, under tenancy and policy, through the same path as the panel’s list page:

cargo run --manifest-path benchmarks/tablo/Cargo.toml -- --bench
./benchmarks/scripts/bench.sh   # adds an HTTP run with `oha`

The target is under 40 ms at the median on local SQLite. It is a reference, not a pass/fail gate: the harness prints the target beside the measurement. Results are written to benchmarks/results/, which is not committed. benchmarks/README.md covers the setup, the optional PostgreSQL run and the method.