Test controllers
You’ll write fast tests that send requests through the full gisila pipeline (routing, binding, forms, guards, error mapping) without starting a server, authenticate as different users, and unit-test a service on its own.
Prerequisites
Section titled “Prerequisites”testindev_dependenciesand generated.g.dartfiles (dart run build_runner build).
1. Build the app in a helper
Section titled “1. Build the app in a helper”Share the registration code between main() and tests so they can’t drift:
import 'package:gisila/gisila.dart';
void configureApp(GisilaApp app) { app.registerSingleton<NoteRepository>(NoteRepository()); app.registerService<NoteService>(NoteService.new); app.registerController(attacher: NotesController().attachToApp);}2. Create a client per test
Section titled “2. Create a client per test”import 'package:gisila/gisila.dart';import 'package:gisila/testing.dart';import 'package:notes_api/app.dart';import 'package:test/test.dart';
void main() { late GisilaApp app; late GisilaTestClient client;
setUp(() async { app = GisilaApp( config: AppConfig(authenticator: TestAuthenticator(), logRequests: false), ); configureApp(app); await app.init(); // factory singletons, onStart hooks, cache client = GisilaTestClient(app); });
tearDown(() => app.stop());
test('creates and reads a note', () async { final created = await client.post('/notes', json: {'title': 'Hi'}); expect(created.statusCode, 201); final id = created.jsonMap['id'];
final read = await client.get('/notes/$id'); expect(read.statusCode, 200); expect(read.jsonMap['title'], 'Hi'); });}GisilaTestClient(app) builds the handler once, so register everything before creating it. It doesn’t call init(); do that yourself when you use factory singletons, onStart hooks or a cache. logRequests: false keeps test output quiet.
3. Authenticate as anyone
Section titled “3. Authenticate as anyone”With TestAuthenticator as the app’s authenticator, client.as(principal) returns a client that is authenticated as that principal. Assuming the delete route carries @Permissions({'notes:write'}):
test('only writers may delete', () async { final reader = client.as(const Principal(id: 'r', permissions: {'notes:read'})); final writer = client.as(const Principal(id: 'w', permissions: {'notes:write'}));
expect((await client.delete('/notes/1')).statusCode, 401); expect((await reader.delete('/notes/1')).errorCode, 'forbidden'); expect((await writer.delete('/notes/1')).statusCode, anyOf(204, 404));});Clients are immutable: as, withToken and withHeaders return new clients.
To test real tokens too, wrap your production authenticator: TestAuthenticator(fallback: jwtAuthenticator), then use client.withToken(signer.sign(subject: 'u1')).
4. Assert on errors
Section titled “4. Assert on errors”final res = await client.post('/notes', json: {});expect(res.statusCode, 400);expect(res.errorCode, 'validation_failed');expect(res.error!['details'], containsPair('title', 'This field is required'));TestResponse gives you statusCode, case-insensitive headers, body, json, jsonMap, jsonList, error, errorCode and errorMessage.
5. Send forms, raw bodies and query parameters
Section titled “5. Send forms, raw bodies and query parameters”await client.post('/login', form: {'email': 'a@b.co', 'password': 'pw'}); // url-encodedawait client.put('/blobs/1', body: [1, 2, 3], contentType: 'application/octet-stream');await client.get('/notes', query: {'limit': 10, 'tag': ['a', 'b']}); // ?limit=10&tag=a&tag=b6. Unit-test a service
Section titled “6. Unit-test a service”A service that takes its dependencies through its constructor is the easiest to unit-test. Register it with a closure in the app, construct it directly in tests, and attach a hand-made context when it reads ctx:
class NoteService extends Service { NoteService(this._repo); final NoteRepository _repo;
Note get(int id) => _repo.find(id) ?? (throw NotFound('Note $id not found')); String get caller => ctx.principal?.id ?? 'anonymous';}
// In the app: app.registerService<NoteService>(() => NoteService(app.singleton<NoteRepository>()));
test('get throws NotFound for unknown ids', () { final service = NoteService(NoteRepository()) ..attach(RequestContext( requestId: 'test', request: Request('GET', Uri.parse('http://localhost/notes/42')), principal: const Principal(id: 'u1'), )); expect(() => service.get(42), throwsA(isA<NotFound>())); expect(service.caller, 'u1');});A hand-made context has no service or singleton resolver, so ctx.service<T>() and singleton<T>() throw StateError there. Test code that relies on them through GisilaTestClient.
Verify it works
Section titled “Verify it works”dart testNext steps
Section titled “Next steps”- Testing utilities reference
- Role-based permissions for guard tests