mcpField
S
serverpod-testing

serverpod-testing

github.com/serverpodTesting & quality3.3k45 installsUpdated 1d ago
Repository

About serverpod-testing

serverpod-testing is a testing guide for Serverpod, aimed at Dart developers validating endpoints and business logic. It covers tests with server context, database rollback behavior, authentication, and stream testing.

Key features

  1. 01Call endpoints with generated test tools
  2. 02Configure sessions and authentication
  3. 03Control test database rollback
  4. 04Test streams and Session-based business logic

Use cases

  1. 01Verify endpoint integration behavior
  2. 02Test business logic that needs a database session
  3. 03Isolate test data and assert stream events

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/serverpod/serverpod --skill serverpod-testing

After installing, type/serverpod-testingin Claude Code to confirm it loaded.

Bundled filesreferences/with-serverpod-options.md

Details

From the project README

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

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
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
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.
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:

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:

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.
  • ephemeralDatabase — own empty database per group, true by default. Set to false to use the configured database, including a previously seeded one. It is not created or dropped, and groups that share it cannot run in parallel.
  • 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.

Running tests

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.

Comments

Markdown supported