-
Notifications
You must be signed in to change notification settings - Fork 0
Testing
ChuckleChest uses a layered testing approach:
| Layer | Location | What's Tested |
|---|---|---|
| Supabase tests | supabase/tests/ |
DB functions, triggers, RLS policies |
| Unit tests | packages/*/test/ |
Repositories, clients in isolation |
| E2E tests | app/test/pages/*/ |
Full app flows with mocked clients |
pgTAP tests for every DB function, trigger, and RLS policy change. More important than Dart tests — if the DB is wrong, the app is wrong.
Run: hobnob db:test
Before writing tests, read supabase/tests/001_helpers.sql — helpers change
frequently, new ones can be added there (add shared helpers there, not inline).
BEGIN;
SELECT no_plan(); -- Always no_plan(), never plan(N)
-- Arrange (one-time setup shared across all scenarios)
SELECT tests.create_chucklechest_user('owner');
SELECT tests.create_chest('owner') AS chest_id \gset
SAVEPOINT arrange_all;
-- Arrange
SELECT tests.authenticate_with_claims_as('owner');
-- Act
SELECT public.some_function(:'chest_id') AS result \gset
-- Assert (one is() / ok() / throws_ok() per requirement)
SELECT is(
(SELECT count(*)::int FROM public.some_table WHERE chest_id = :'chest_id'),
1,
'Given: owner authenticated. When: some_function called. Then: record created.'
);
ROLLBACK TO arrange_all;
-- (next scenario from same base state)
SELECT * FROM finish();
ROLLBACK;-
no_plan()always — neverplan(N) -
SAVEPOINT arrange_all+ROLLBACK TO arrange_allbetween scenarios — resets to shared base state - One
is()/ok()/throws_ok()per test — one requirement per assertion - Test descriptions:
'Given: ... When: ... Then: ...' - AAA comments on every scenario:
-- Arrange,-- Act,-- Assert - Use
\gsetto capture return values:SELECT fn() AS var \gset→:var - Authenticate before every Act:
tests.authenticate_with_claims_as('user')ortests.authenticate_as_service_role()
End-to-end tests pump the full app with real repositories, real cubits, and real routing — only the data clients are mocked. This means tests exercise the actual state management and navigation without caring whether it's BLoC, Riverpod, or anything else.
E2E tests verify user flows (navigation, interactions, page transitions),
not UI appearance. Widget visibility checks (e.g. finding a
CircularProgressIndicator) are used only to confirm the correct page/state is
shown.
| File | Purpose |
|---|---|
test_clients.dart |
CTestClients — mock clients with defaults |
pump_app.dart |
pumpChuckleChestApp() — WidgetTester extension |
mock_storage.dart |
buildMockStorage() — HydratedBloc mock |
helpers.dart |
Barrel export |
Creates mocked versions of all six data clients. The constructor stubs
authClient.currentUserStream to emit a signed-out state by default. Override
individual method stubs in your test's setUp or inline:
late CTestClients clients;
setUp(() {
clients = CTestClients();
when(
() => clients.authClient.logInWithOTP(email: any(named: 'email')),
).thenReturn(bobsFakeSuccessJob(bobsNothing));
});Extension on WidgetTester that:
- Sets up mock
HydratedBloc.storage - Wraps the app in
CAppDependenciesProviderwith mock clients - Builds
ChuckleChestAppwith thedevelopmentflavor - Calls
pumpAndSettle()
Supports startAt parameter for deep linking to a specific route:
await tester.pumpChuckleChestApp(
clients: clients,
startAt: '/signin/login',
);CAppDependenciesProvider accepts optional client overrides. In production, it
creates clients from the Supabase instance. In tests, mock clients are injected
directly — no Supabase connection needed.
Tests go in app/test/pages/<page_name>/. Follow this pattern:
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:test_beautifier/test_beautifier.dart';
import '../helpers/helpers.dart';
void main() {
group('Feature Tests', () {
late CTestClients clients;
setUp(() {
clients = CTestClients();
});
testWidgets(
requirement(
given: 'some precondition',
whenever: 'user does something',
then: 'expected outcome',
why: 'reason this matters',
),
(tester) async {
// Stub client methods as needed
when(() => clients.authClient.someMethod())
.thenReturn(bobsFakeSuccessJob(someValue));
// Pump the app
await tester.pumpChuckleChestApp(clients: clients);
// Interact
await tester.tap(find.byKey(const Key('some_button')));
await tester.pumpAndSettle();
// Assert (find.byType() OK only for unique codebase-owned page widgets)
expect(find.byType(ExpectedPage), findsOneWidget);
},
);
});
}- Mock at the client level — repositories, cubits, and routing stay real
-
Use
find.byKey()for all widget lookups. Only usefind.byType()when the type is unique and defined in this codebase (e.g. page widgets likeCHomePage). Never usefind.byText()orfind.byIcon()— they couple tests to localized strings or icon choices. Addkey:parameters to production widgets as needed and define the key constants at the top of the test file with explicit types:const Key _myKey = Key('my_key'); - State management agnostic — tests interact via widgets, not via cubits/blocs directly
- No UI appearance tests — only verify widget presence and navigation
- Use
test_beautifier'srequirement()for BDD-style test descriptions - Use
bobsFakeSuccessJob()/bobsFakeFailureJob()frombobs_jobsto stub client methods
hobnob dart:testSee also: Architecture · Authentication