LogoSkills

Backend Test Patterns

Patterns applied when writing Serverpod backend integration tests.

Patterns applied when writing Serverpod backend integration tests.

Directory Structure#

backend/kobic_server/test/
├── integration/
│   ├── helpers/
│   │   ├── test_auth_helper.dart      # Authenticated session creation
│   │   ├── test_data_factory.dart     # Test data creation/cleanup
│   │   └── test_scope_helper.dart     # Multi-tenant scope validation
│   ├── payments/                      # Payment endpoint tests
│   ├── books/                         # Book endpoint tests
│   ├── test_tools/
│   │   └── serverpod_test_tools.dart  # Serverpod auto-generated
│   └── {feature}_test.dart            # Other integration tests
└── unit/
    └── {feature}/                     # Service unit tests

Test Levels#

LevelPurposeComplexity
L1 SmokeVerify authentication requirementLow
L2 Happy PathCore method normal operationMedium
L3 ComprehensiveEdge cases + securityHigh

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#

  1. Use sessionBuilder: Endpoint methods receive sessionBuilder (not session)
  2. Auth setup: Set AuthenticationInfo via session.updateAuthenticated()
  3. Cleanup required: Clean up created data and sessions in finally blocks
  4. Direct DB access: Use UserInfo.db.insertRow(), UserInfo.db.deleteRow() patterns
  5. Unique identifiers: Use TestDataFactory.uniqueId('prefix')
  • backend/kobic_server/test/integration/helpers/test_auth_helper.dart
  • backend/kobic_server/test/integration/helpers/test_data_factory.dart
  • backend/kobic_server/test/integration/helpers/test_scope_helper.dart
  • docs/architecture-test-coverage-initiative-2026-03-13.md