serverpod-testing

star 3.2k

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.

serverpod By serverpod schedule Updated 5/5/2026

name: serverpod-testing description: 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.

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');
    });
  });
}

Start required services before running tests. In default Docker/PostgreSQL projects this is usually docker compose up -d, then dart test; SQLite or Mini projects may not need Docker, and Redis is only needed for Redis/global-cache/message tests.

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). Clean up manually in tearDownAll; consider --concurrency=1.
withServerpod(
  'Given concurrent transactions',
  (sessionBuilder, endpoints) {
    tearDownAll(() async {
      var session = sessionBuilder.build();
      await Product.db.deleteWhere(session, where: (_) => Constant.bool(true));
    });

    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

Option Default Description
applyMigrations true Apply pending migrations on start
configOverride Override loaded server config for tests
enableSessionLogging false Enable session logging
experimentalFeatures null Experimental features to enable for the tests
rollbackDatabase afterEach When to rollback (afterEach, afterAll, disabled)
runMode ServerpodRunMode.test Run mode (test, development, etc.)
runtimeParametersBuilder null Override global runtime parameters for the tests
serverpodLoggingMode normal Logging mode
serverpodStartTimeout 30s Timeout for Serverpod startup
testGroupTagsOverride ['integration'] Tags for the test group
testServerOutputMode normal Control stdout/stderr from the test server

Running tests

docker compose up -d          # Start DB and Redis
dart test                     # All tests
dart test -t integration      # Only integration tests
dart test -x integration      # Only unit tests
dart test -t integration --concurrency=1  # Sequential (for rollback disabled)

DB connection limits

Each withServerpod lazily creates a Serverpod instance on first sessionBuilder.build(). With many concurrent tests, DB connections can exceed limits. Fix: raise the DB limit, or defer build() to setUpAll:

withServerpod('Given example', (sessionBuilder, endpoints) {
  late Session session;
  setUpAll(() { session = sessionBuilder.build(); });
  // ...
});

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.

Install via CLI
npx skills add https://github.com/serverpod/serverpod --skill serverpod-testing
Repository Details
star Stars 3,207
call_split Forks 364
navigation Branch main
article Path SKILL.md
More from Creator