Skip to main content

supabase-test-testing

Writes unit and widget tests for Dart and Flutter code that uses Supabase, with the supabase_test package: a real SupabaseClient whose HTTP requests are answered by stubs and whose realtime socket runs in memory. Use when testing code that queries tables, calls rpc or edge functions, signs users in, uses storage or subscribes to realtime, or when replacing hand-written mocks of the Supabase client or mock_supabase_http_client.

Quellinformationen

Repository
supabase/supabase-flutter
Letzte Quellaktivität
23. Oktober 2026 um 15:21
Erkannte Sprache von SKILL.md
Englisch
Sterne
1.040
Forks
313

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
name
supabase-test-testing
description
Writes unit and widget tests for Dart and Flutter code that uses Supabase, with the supabase_test package: a real SupabaseClient whose HTTP requests are answered by stubs and whose realtime socket runs in memory. Use when testing code that queries tables, calls rpc or edge functions, signs users in, uses storage or subscribes to realtime, or when replacing hand-written mocks of the Supabase client or mock_supabase_http_client.
# Testing Supabase code with supabase_test ## Rules - Test against a real client from `testSupabaseClient`, or `initializeTestSupabase` in a Flutter widget test. Never mock or subclass `SupabaseClient`, `AuthClient` or the query builders, and never subclass `http.BaseClient` for Supabase requests. - Answer requests with the `stub…` methods of `MockSupabaseHttpClient`. Stub the rows the code under test should receive: filters are not evaluated. - Put a client into a signed-in state with `signInTestUser`. Build auth payloads with `testUserJson` and `testSessionResponseJson`, and tokens with `unsignedTestJwt`. Never hand-assemble a JWT or session JSON. - Assert on what was sent with `requests` and `requestsTo`, not by wrapping the client. - Dispose every client at the end of the test. - Match methods with `HttpMethod.post` and friends. ## Setup Add `supabase_test` as a dev dependency, next to `test`, or `flutter_test` in a Flutter app: ```sh dart pub add dev:supabase_test dev:test ``` Use `flutter pub add dev:supabase_test` in a Flutter app, and import `package:flutter_test/flutter_test.dart` instead of `package:test/test.dart`. The examples query the `Books` table of the schema `supabase_typegen` generates into `lib/supabase_schema.g.dart`. Replace `my_app` with the name of the package under test. ```dart test-file import 'package:my_app/supabase_schema.g.dart'; import 'package:supabase/supabase.dart'; import 'package:supabase_test/supabase_test.dart'; import 'package:test/test.dart'; void main() { late MockSupabaseHttpClient httpClient; late SupabaseClient supabase; setUp(() { httpClient = MockSupabaseHttpClient(); supabase = testSupabaseClient(httpClient: httpClient); addTearDown(supabase.dispose); }); test('loads the books in print', () async { httpClient.stubTable( Books.table.name, rows: [ {'id': 1, 'author_id': 2, 'title': 'Dune', 'in_print': true}, ], ); final books = await supabase .table(Books.table) .selectOnly([Books.id, Books.title]) .where(Books.inPrint.isTrue()); expect(books.single.read(Books.title), 'Dune'); }); } ``` The stubbed rows are the JSON PostgREST would return, with database column names, so a typed query decodes them exactly like a real response. Stub every column the query reads: a `select()` that reads all columns of `BooksRow` needs every non-nullable column in each row. ## Stubbing | Endpoint | Stub | | ------------------------------------ | ---------------------------------------------------------------------------------------------------------- | | A table | `stubTable('books', rows: [...])` | | A database function | `stubRpc('book_by_id', body: {...})` | | An edge function | `stubEdgeFunction('hello', body: {...})` | | Sign in, sign up, sign out, the user | `stubSignIn()`, `stubSignUp()`, `stubSignOut()`, `stubUser()` | | Storage | `stubStorageUpload`, `stubStorageDownload`, `stubStorageSignedUrl`, `stubStorageList`, `stubStorageRemove` | | Any other path | `stub(body, method: HttpMethod.get, path: '/storage/v1/bucket/avatars')` | | A response computed from the request | `stubHandler((request) => jsonResponse(...), path: ...)` | | No network | `stubError(ClientException('Offline'))` | | A request that never answers | `stubStall(path: ...)` | | Retries | `stubStatuses([503, 503, 200], body: [...], path: ...)` | - The latest matching stub wins, so register defaults in `setUp` and override them in a test. - `times:` limits how often a stub answers. Register the later responses first to answer a sequence. - `query:` narrows a stub to requests carrying those query parameters, for example `query: {'id': 'eq.1'}`. - An unmatched request throws a `StateError` naming the request and the registered stubs, and fails the test even when a client wraps the error. - `testSupabaseClient` does not retry, so a failure surfaces at once. Pass `retryOptions` to test retry handling. - `single()` receives the only row of a one-row stub, and a count query receives the number of rows, or `count:` when it is given. Errors are stubbed with the status code and the service's error body: ```dart test final httpClient = MockSupabaseHttpClient() ..stubTable( 'books', rows: {'message': 'permission denied', 'code': '42501'}, statusCode: 403, ); final supabase = testSupabaseClient(httpClient: httpClient); await expectLater( () => supabase.table(Books.table).select(), throwsA(isA<PostgrestApiException>()), ); ``` ## Asserting on requests ```dart test final httpClient = MockSupabaseHttpClient()..stubTable('books'); final supabase = testSupabaseClient(httpClient: httpClient); await supabase .table(Books.table) .insert(BooksInsert(authorId: 2, title: 'Dune')); final insert = httpClient .requestsTo('/rest/v1/books', method: HttpMethod.post) .single; expect(insert.jsonBody, {'author_id': 2, 'title': 'Dune'}); ``` `requests` holds every answered request with its body already read, `queryParameters` and `headers` are on each one, and `reset()` clears stubs and recorded requests. ## Auth Run a test as a signed-in user without any network traffic: ```dart test final httpClient = MockSupabaseHttpClient()..stubTable('books', rows: []); final supabase = testSupabaseClient(httpClient: httpClient); final session = await signInTestUser( supabase.auth, userId: 'user-1', email: 'someone@example.com', ); await supabase.table(Books.table).select(); expect( httpClient.requests.last.headers['Authorization'], 'Bearer ${session.accessToken}', ); ``` To test a sign-in flow itself, stub the endpoint and call the real method: ```dart test final httpClient = MockSupabaseHttpClient() ..stubSignIn(user: testUserJson(id: 'user-1')); final supabase = testSupabaseClient(httpClient: httpClient); final session = await supabase.auth.signInWithPassword( email: 'someone@example.com', password: 'password', ); expect(session.user.id, 'user-1'); ``` ## Realtime Pass a `MockRealtimeTransport`, an in-memory realtime server, and push server events from the test: ```dart test final realtime = MockRealtimeTransport(); final supabase = testSupabaseClient( httpClient: MockSupabaseHttpClient(), realtimeTransport: realtime.call, ); final channel = supabase.channel('books'); final changes = channel.onTableChanges(Books.table); channel.subscribe(); await channel.onStatusChange.firstWhere( (change) => change.status == RealtimeSubscribeStatus.subscribed, ); realtime.emitPostgresChange( table: 'books', event: PostgresChangeEvent.insert, newRecord: {'id': 1, 'author_id': 2, 'title': 'Dune', 'in_print': true}, ); final change = await changes.first; expect(change, isA<TypedPostgresInsert<BooksRow, BooksRow>>()); ``` A `stream()` test stubs the initial rows with `stubTable` and then emits the changes it wants applied. `emitBroadcast` delivers a broadcast message, `emitPresenceState` and `emitPresenceDiff` deliver presence, `closeConnection` drops the socket, and `sent` and `joinedTopics` record what the client sent. ## Flutter widget tests `initializeTestSupabase` from `package:supabase_flutter/testing.dart` initializes `Supabase.instance` with the same test defaults, so widgets that read `Supabase.instance.client` work unchanged: ```dart test-file import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:my_app/supabase_schema.g.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:supabase_flutter/testing.dart'; import 'package:supabase_test/supabase_test.dart'; class BookTitles extends StatefulWidget { const BookTitles({super.key}); @override State<BookTitles> createState() => _BookTitlesState(); } class _BookTitlesState extends State<BookTitles> { final _books = Supabase.instance.client .table(Books.table) .selectOnly([Books.id, Books.title]); @override Widget build(BuildContext context) { return FutureBuilder( future: _books, builder: (context, snapshot) => Column( children: [ for (final book in snapshot.data ?? const []) Text(book.read(Books.title), textDirection: TextDirection.ltr), ], ), ); } } void main() { testWidgets('shows the book titles', (tester) async { final httpClient = MockSupabaseHttpClient() ..stubTable( 'books', rows: [ {'id': 1, 'title': 'Dune'}, ], ); final supabase = await initializeTestSupabase(httpClient: httpClient); addTearDown(supabase.dispose); await tester.pumpWidget(const BookTitles()); await tester.pumpAndSettle(); expect(find.text('Dune'), findsOneWidget); }); } ``` ## Integration tests Mocks are for fast unit tests. For end-to-end coverage, run the tests against a local stack from `supabase start` and point a regular client at the URL and keys it prints.
Auf GitHub ansehen