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/loginonce and reuse the client’s cookies, or insert anAuthSessionrow directly to skip the password hash. - Declaration mistakes. A panel that refuses to mount returns a
MountErrorinside the router builder’s error. Downcast to it and match on each mistake’sDeclarationErrorKindrather 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.