Testing utilities
import 'package:gisila/gisila.dart';import 'package:gisila/testing.dart';import 'package:test/test.dart';package:gisila/testing.dart is a separate library: import it from tests only.
GisilaTestClient
Section titled “GisilaTestClient”An in-memory HTTP client. Requests go through app.buildHandler(), so guards, forms, middleware and error mapping all run, with no server or socket.
class GisilaTestClient { GisilaTestClient(GisilaApp app, {Map<String, String> headers = const {}, Uri? baseUri}); GisilaTestClient.handler(Handler handler, {Map<String, String> headers = const {}, Uri? baseUri});
final Map<String, String> headers; final Uri baseUri;
GisilaTestClient withToken(String token); GisilaTestClient withHeaders(Map<String, String> extra); GisilaTestClient as(Principal principal);
Future<TestResponse> get(String path, {Map<String, Object?>? query, Map<String, String>? headers}); Future<TestResponse> head(String path, {...}); Future<TestResponse> options(String path, {...}); Future<TestResponse> post(String path, {Object? json, Map<String, String>? form, Object? body, String? contentType, Map<String, Object?>? query, Map<String, String>? headers}); Future<TestResponse> put(String path, {...}); Future<TestResponse> patch(String path, {...}); Future<TestResponse> delete(String path, {Object? json, Map<String, Object?>? query, Map<String, String>? headers}); Future<TestResponse> request(String method, String path, {...});}When to use: for nearly every controller test. It exercises the real routing, binding and guards while staying fast. Use a real server only to test things that need sockets (client addresses, TLS).
| Member | Description |
|---|---|
GisilaTestClient(app) |
Builds the app’s handler once, so register controllers, services and singletons first. It does not call init(): do that yourself if you use factory singletons, onStart hooks or a cache. |
GisilaTestClient.handler(handler) |
A client for any Shelf handler. as() is not available. |
headers |
Sent with every request; per-request headers win. |
baseUri |
Scheme and host; defaults to http://localhost. |
withToken(token) |
New client sending Authorization: Bearer <token>. |
withHeaders(extra) |
New client with extra headers. Clients are immutable, so a shared base client can’t leak credentials between tests. |
as(principal) |
New client authenticated as principal. Needs AppConfig(authenticator: TestAuthenticator(...)), otherwise StateError. |
request(...) |
Give at most one body: json (encoded, application/json), form (url-encoded) or raw body (String or bytes) with contentType. query values may be lists for repeated keys; null values are skipped. |
Requests have no connection info, so the rate limiter’s default key is the same (unknown) for every test request.
Example
void main() { late GisilaApp app; late GisilaTestClient client; final auth = TestAuthenticator();
setUp(() async { app = GisilaApp(config: AppConfig(authenticator: auth, logRequests: false)); app.registerService<UserService>(UserService.new); app.registerController(attacher: UsersController().attachToApp); await app.init(); client = GisilaTestClient(app); });
tearDown(() => app.stop());
test('requires users:read', () async { final res = await client.get('/users/1'); expect(res.statusCode, 401); expect(res.errorCode, 'unauthorized'); });
test('admins can read users', () async { final admin = client.as(const Principal(id: 'a', permissions: {'users:read'})); final res = await admin.get('/users/1'); expect(res.statusCode, 200); expect(res.jsonMap['id'], '1'); });
test('validates the form', () async { final res = await client.post('/users', json: {'email': 'nope'}); expect(res.statusCode, 400); expect(res.errorCode, 'validation_failed'); expect((res.error!['details'] as Map)['email'], isNotNull); });}TestResponse
Section titled “TestResponse”A fully read response.
class TestResponse { final int statusCode; final Map<String, String> headers; final List<int> bodyBytes; late final String body; late final Object? json; Map<String, Object?> get jsonMap; List<Object?> get jsonList; Map<String, Object?>? get error; String? get errorCode; String? get errorMessage;}| Member | Description |
|---|---|
statusCode |
Response status. |
headers |
Response headers; lookups ignore case. |
bodyBytes, body |
Raw bytes and UTF-8 text. |
json |
Decoded JSON. Throws FormatException (showing the body) when the body isn’t JSON. |
jsonMap, jsonList |
json as an object or array; StateError otherwise. |
error |
The error object of the gisila envelope, or null. |
errorCode, errorMessage |
error.code and error.message. |
TestAuthenticator
Section titled “TestAuthenticator”An authenticator for tests: tokenFor hands out a bearer token for any principal.
class TestAuthenticator extends Authenticator { TestAuthenticator({Authenticator fallback = const NullAuthenticator()}); final Authenticator fallback; String tokenFor(Principal principal);}When to use: set it as the app’s authenticator in tests so client.as(principal) works. Requests without a test token go to fallback, so you can wrap your real authenticator and test real tokens alongside.
final auth = TestAuthenticator( fallback: JwtAuthenticator(key: JwtKey.hmac(testSecret), algorithms: {'HS256'}),);See also: Test controllers guide.