Backend Test Patterns
Patterns applied when writing Serverpod backend integration tests.
Patterns applied when writing Serverpod backend integration tests.
Directory Structure#
Read .cob/feature-layout.json first; preserve it. Without a marker use cob's existing legacy-tree detection, otherwise bundle. Follow
the generation placement contract. Pure module tests live at
feature/<d>/server/test/unit/; any Session/DB/withServerpod test belongs to the executable host and
bundle.json.hostTests. Host ports are session-aware and unimplemented adapters fail closed.
feature/<d>/server/test/unit/ # Pure service/policy/validation; no DB bootstrap
backend/<project>_server/test/integration/
├── helpers/
│ ├── test_auth_helper.dart # Host auth/provider mapping
│ ├── test_data_factory.dart # Test data creation/cleanup
│ └── test_scope_helper.dart # Multi-tenant scope validation
├── test_tools/serverpod_test_tools.dart
└── <d>_test.dart # DB/dispatcher/HTTP/SDK integration
Test Levels#
| Level | Purpose | Complexity |
|---|---|---|
| L1 Smoke | Verify authentication requirement | Low |
| L2 Happy Path | Core method normal operation | Medium |
| L3 Comprehensive | Edge cases + security | High |
L1 Smoke Test Pattern#
Verifies that ServerpodUnauthenticatedException is thrown when called without authentication.
import 'package:test/test.dart';
import '../test_tools/serverpod_test_tools.dart';
void main() {
withServerpod('Given MyEndpoint endpoint', (sessionBuilder, endpoints) {
group('L1 Smoke: Exception when called without auth', () {
test('myMethod requires auth', () async {
await expectLater(
endpoints.myEndpoint.myMethod(sessionBuilder),
throwsA(isA<ServerpodUnauthenticatedException>()),
);
});
});
});
}
L2 Happy Path Pattern#
Creates an authenticated session with TestAuthHelper and tests core logic.
import 'package:test/test.dart';
import '../helpers/test_auth_helper.dart';
import '../test_tools/serverpod_test_tools.dart';
void main() {
withServerpod('Given MyEndpoint endpoint', (sessionBuilder, endpoints) {
group('L2 Happy Path: Authenticated user normal operation', () {
test('myMethod returns expected result', () async {
final auth = await TestAuthHelper.createAdminSession(sessionBuilder);
try {
final result = await endpoints.myEndpoint.myMethod(sessionBuilder);
expect(result, isNotNull);
} finally {
await TestAuthHelper.cleanup(
auth.session,
userInfo: auth.userInfo,
);
}
});
});
});
}
TestAuthHelper Usage#
// Admin session
final admin = await TestAuthHelper.createAdminSession(sessionBuilder);
// Publisher session
final publisher = await TestAuthHelper.createPublisherSession(
sessionBuilder,
publisherId: 42,
);
// Regular user session
final user = await TestAuthHelper.createUserSession(sessionBuilder);
// Unauthenticated session
final session = TestAuthHelper.createUnauthenticatedSession(sessionBuilder);
// Cleanup (call in finally block)
await TestAuthHelper.cleanup(admin.session, userInfo: admin.userInfo);
TestDataFactory Usage#
final factory = TestDataFactory(session);
// Create test data + register cleanup callback
factory.onCleanup(() async {
await MyModel.db.deleteRow(session, createdRow);
});
// Clean up all data in reverse order
await factory.cleanup();
Key Rules#
-
Use
sessionBuilder: Endpoint methods receivesessionBuilder(not session) - Auth setup: Set AuthenticationInfo via
session.updateAuthenticated() - Cleanup required: Clean up created data and sessions in finally blocks
-
Direct DB access: Use
UserInfo.db.insertRow(),UserInfo.db.deleteRow()patterns - Unique identifiers: Use
TestDataFactory.uniqueId('prefix')
Related Files#
backend/<project>_server/test/integration/helpers/test_auth_helper.dartbackend/<project>_server/test/integration/helpers/test_data_factory.dartbackend/<project>_server/test/integration/helpers/test_scope_helper.dartdocs/architecture-test-coverage-initiative-2026-03-13.md