packages/serverpod/skills/serverpod-testing/SKILL.md
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.
npx skillsauth add serverpod/serverpod serverpod-testingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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.
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.
Use sessionBuilder.copyWith(...) to create modified sessions. Call sessionBuilder.build() to get a Session for DB operations or passing to helpers.
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>()),
);
});
});
});
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.
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,
);
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>()),
);
});
});
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]));
});
});
| 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 |
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)
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(); });
// ...
});
Keep tests organized:
test/unit/ — unit tests (no Serverpod dependency)test/integration/ — tests using withServerpodAlways call endpoints via the endpoints parameter, not by instantiating endpoint classes directly — the test tools handle lifecycle and validation to match production behavior.
development
Build highly distinctive, production-ready Flutter interfaces with exceptional design fidelity. Include this skill whenever a user requests Flutter widgets, screens, or full apps.
testing
Serverpod Authentication — Signing in users, verify if they are authenticated, assinging scopes (e.g., admin). Use when adding features that require the user to be signed in.
development
Serverpod web server (Relic) — REST APIs, webhooks, middleware, static files, server-rendered HTML, SPAs, Flutter web. Use when adding HTTP routes, serving web pages or web apps, intercepting requests, or working with the Relic web server.
tools
Serverpod overview — what it is, project structure, how to work with. Always use at least once when working with projects that use Serverpod.