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 concurrentsession.db.transaction(...)calls (nested transactions would throwInvalidConfigurationException). Clean up manually intearDownAll; 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 usingwithServerpod
Always call endpoints via the endpoints parameter, not by instantiating endpoint classes directly — the test tools handle lifecycle and validation to match production behavior.