# Serverpod Testing

> Test Serverpod endpoints and business logic - withServerpod, sessionBuilder, authentication, DB seeding, rollback, streams, running tests. Use when writing server tests or working with serverpod_test.

- Skill: `serverpod/serverpod-testing` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add serverpod/serverpod-testing`
- Raw SKILL.md: https://api.skillmd.com/api/skills/serverpod/serverpod-testing/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: serverpod (https://skillmd.com/u/serverpod)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/serverpod/serverpod-testing

---


# Serverpod Testing

Generated test tools let you call endpoints in tests with full server context (DB, caching, etc.). Import the **generated** test tools file, not `serverpod_test` directly - it re-exports everything needed. The import path comes from `config/generator.yaml` (`server_test_tools_path`); the examples below use the common `test_tools/serverpod_test_tools.dart` output.

Prefer Given/when/then descriptions. Across nested groups plus the test name, there should be one clear Given, one when, and one then that can explain a failure without reading the code.

## Basic test

```dart
import 'package:test/test.dart';
import 'test_tools/serverpod_test_tools.dart';

void main() {
  withServerpod('Given Greeting endpoint', (sessionBuilder, endpoints) {
    test('when calling hello then returns greeting', () async {
      final greeting = await endpoints.greeting.hello(sessionBuilder, 'Bob');
      expect(greeting.message, 'Hello Bob');
    });
  });
}
```

## Session builder

Use `sessionBuilder.copyWith(...)` to create modified sessions. Call `sessionBuilder.build()` to get a `Session` for DB operations or passing to helpers.

### Authenticated tests

```dart
withServerpod('Given AuthEndpoint', (sessionBuilder, endpoints) {
  final userId = '550e8400-e29b-41d4-a716-446655440000';

  group('when authenticated', () {
    var authed = sessionBuilder.copyWith(
      authentication: AuthenticationOverride.authenticationInfo(userId, {Scope('user')}),
    );

    test('then hello succeeds', () async {
      final greeting = await endpoints.authExample.hello(authed, 'Michael');
      expect(greeting, 'Hello, Michael!');
    });
  });

  group('when unauthenticated', () {
    var unauthed = sessionBuilder.copyWith(
      authentication: AuthenticationOverride.unauthenticated(),
    );

    test('then hello throws', () async {
      await expectLater(
        endpoints.authExample.hello(unauthed, 'Michael'),
        throwsA(isA<ServerpodUnauthenticatedException>()),
      );
    });
  });
});
```

### Seeding the database

```dart
withServerpod('Given Products endpoint', (sessionBuilder, endpoints) {
  var session = sessionBuilder.build();

  setUp(() async {
    await Product.db.insert(session, [
      Product(name: 'Apple', price: 10),
      Product(name: 'Banana', price: 10),
    ]);
  });

  test('then all returns both products', () async {
    final products = await endpoints.products.all(sessionBuilder);
    expect(products, hasLength(2));
  });
});
```

No manual tearDown needed - by default each test runs in a transaction that is rolled back.

## Rollback behavior

Default: `RollbackDatabase.afterEach` - each test in a rolled-back transaction.

- `afterAll` - roll back after all tests in the group. Useful for scenario tests where consecutive tests depend on each other and setup is expensive.
- `disabled` - no automatic rollback. Required when endpoint code uses concurrent `session.db.transaction(...)` calls (nested transactions would throw `InvalidConfigurationException`). Each `withServerpod` group gets its own database, so committed data never leaks into other groups and no manual cleanup or `--concurrency=1` is needed; clean up in `tearDown` only if later tests in the same group need a clean slate.

```dart
withServerpod(
  'Given concurrent transactions',
  (sessionBuilder, endpoints) {
    test('then should commit all', () async {
      await endpoints.products.concurrentTransactionCalls(sessionBuilder);
    });
  },
  rollbackDatabase: RollbackDatabase.disabled,
);
```

## Testing business logic with Session

If logic lives outside endpoints but needs a `Session`, use `withServerpod` and ignore the endpoints parameter:

```dart
withServerpod('Given product quantity is zero', (sessionBuilder, _) {
  var session = sessionBuilder.build();

  setUp(() async {
    await Product.db.insertRow(session, Product(id: 123, name: 'Apple', quantity: 0));
  });

  test('then decreasing throws', () async {
    await expectLater(
      ProductsBusinessLogic.updateQuantity(session, id: 123, decrease: 1),
      throwsA(isA<InvalidOperationException>()),
    );
  });
});
```

## Testing streams

Use `flushEventQueue()` to ensure a generator executes up to its `yield` before asserting:

```dart
withServerpod('Given shared stream', (sessionBuilder, endpoints) {
  final user1 = sessionBuilder.copyWith(
    authentication: AuthenticationOverride.authenticationInfo('user-1', {}));
  final user2 = sessionBuilder.copyWith(
    authentication: AuthenticationOverride.authenticationInfo('user-2', {}));

  test('when posting numbers then listener receives them', () async {
    var stream = endpoints.comm.listenForNumbers(user1);
    await flushEventQueue(); // Wait for stream to register

    await endpoints.comm.postNumber(user2, 111);
    await endpoints.comm.postNumber(user2, 222);

    await expectLater(stream.take(2), emitsInOrder([111, 222]));
  });
});
```

## withServerpod options

The ones that change behavior most often:

- `rollbackDatabase` — `afterEach` (default), `afterAll`, or `disabled`.
- `applyMigrations` — apply pending migrations on start, `true` by default.
- `runMode` — `ServerpodRunMode.test` by default, so `config/test.yaml` is loaded.
- `configOverride` — adjust the loaded config, e.g. to point at another database.

The full list, with defaults, is in [`references/with-serverpod-options.md`](references/with-serverpod-options.md).

## Running tests

```bash
dart test                     # All tests
dart test -t integration      # Only integration tests
dart test -x integration      # Only unit tests
```

**Nothing has to be started first — do NOT run `docker compose up`.** `withServerpod` boots the server in the `test` run mode, and the test database comes with it: projects created by `serverpod create` use an embedded PostgreSQL that Serverpod launches and manages, or a SQLite file. A generated `docker-compose.yaml` is present even in those projects, so seeing that file is not a reason to start Docker.

Only when a run actually fails to reach the database is it worth opening `config/test.yaml`: a `database` section with `host`/`port` and no `dataPath` points at an external database, which does have to be running (`docker compose up -d` starts the one the project ships). With `dataPath` or `filePath` set, fix the configuration or runtime instead — starting Docker will not help.

## Project structure

Keep tests organized:

- `test/unit/` - unit tests (no Serverpod dependency)
- `test/integration/` - tests using `withServerpod`

Always call endpoints via the `endpoints` parameter, not by instantiating endpoint classes directly - the test tools handle lifecycle and validation to match production behavior.

