serverpod-testing

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Serverpod Testing

Serverpod 测试

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.
生成的测试工具允许你在测试中调用端点,并具备完整的服务器上下文(数据库、缓存等)。请导入生成的测试工具文件,而非直接导入
serverpod_test
——它会重新导出所有所需内容。导入路径来自
config/generator.yaml
中的
server_test_tools_path
;以下示例使用常见的输出路径
test_tools/serverpod_test_tools.dart
。
推荐使用Given/when/then格式描述测试。在嵌套分组和测试名称中,应包含清晰的Given(给定条件)、when(操作)和then(预期结果),无需阅读代码即可解释失败原因。

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');
    });
  });
}
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.
使用
sessionBuilder.copyWith(...)
创建修改后的会话。调用
sessionBuilder.build()
可获取用于数据库操作或传递给辅助函数的
Session
对象。

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>()),
      );
    });
  });
});
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.
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));
  });
});
无需手动执行tearDown——默认情况下,每个测试都会在事务中运行,测试结束后事务会回滚。

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,
);
默认值:
RollbackDatabase.afterEach
——每个测试都在可回滚的事务中运行。
  • afterAll
    ——在分组内所有测试完成后回滚。适用于场景测试,此类测试中连续的测试相互依赖,且初始化成本较高。
  • disabled
    ——禁用自动回滚。当端点代码使用并发的
    session.db.transaction(...)
    调用时需要启用此选项(嵌套事务会抛出
    InvalidConfigurationException
    )。每个
    withServerpod
    分组都有自己的数据库,因此提交的数据永远不会泄露到其他分组中,无需手动清理或设置
    --concurrency=1
    ;仅当同一分组中的后续测试需要干净环境时,才需在
    tearDown
    中进行清理。
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

使用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>()),
    );
  });
});
如果业务逻辑位于端点之外但需要
Session
,可使用
withServerpod
并忽略endpoints参数:
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]));
  });
});
使用
flushEventQueue()
确保生成器在断言前执行到
yield
步骤:
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

withServerpod 选项

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
.
最常改变行为的选项:
  • rollbackDatabase
    —— 可选值为
    afterEach
    (默认)、
    afterAll
    或
    disabled
    。
  • applyMigrations
    —— 启动时应用待处理的迁移,默认值为
    true
    。
  • runMode
    —— 默认值为
    ServerpodRunMode.test
    ,因此会加载
    config/test.yaml
    配置文件。
  • configOverride
    —— 调整加载的配置,例如指向其他数据库。
完整的选项列表及默认值请查看
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.
bash
dart test                     # 所有测试
dart test -t integration      # 仅运行集成测试
dart test -x integration      # 仅运行单元测试
无需提前启动任何服务——不要运行
docker compose up
。
withServerpod
会在
test
运行模式下启动服务器,测试数据库会随服务器一同启动:通过
serverpod create
创建的项目会使用Serverpod启动和管理的嵌入式PostgreSQL,或SQLite文件。即使这些项目中存在生成的
docker-compose.yaml
文件,也无需启动Docker。
只有当测试确实无法连接到数据库时,才需要查看
config/test.yaml
:如果
database
部分包含
host
/
port
且没有
dataPath
,则指向外部数据库,此时需要启动该数据库(
docker compose up -d
可启动项目自带的数据库)。如果设置了
dataPath
或
filePath
,则应修复配置或运行环境——启动Docker无法解决问题。

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.
保持测试结构清晰:
  • test/unit/
    - 单元测试(无Serverpod依赖)
  • test/integration/
    - 使用
    withServerpod
    的测试
始终通过
endpoints
参数调用端点,而非直接实例化端点类——测试工具会处理生命周期和验证,以匹配生产环境的行为。