serverpod-testing
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseServerpod 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 directly - it re-exports everything needed. The import path comes from (); the examples below use the common output.
serverpod_testconfig/generator.yamlserver_test_tools_pathtest_tools/serverpod_test_tools.dartPrefer 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_testconfig/generator.yamlserver_test_tools_pathtest_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 to create modified sessions. Call to get a for DB operations or passing to helpers.
sessionBuilder.copyWith(...)sessionBuilder.build()Session使用创建修改后的会话。调用可获取用于数据库操作或传递给辅助函数的对象。
sessionBuilder.copyWith(...)sessionBuilder.build()SessionAuthenticated 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: - each test in a rolled-back transaction.
RollbackDatabase.afterEach- - roll back after all tests in the group. Useful for scenario tests where consecutive tests depend on each other and setup is expensive.
afterAll - - no automatic rollback. Required when endpoint code uses concurrent
disabledcalls (nested transactions would throwsession.db.transaction(...)). EachInvalidConfigurationExceptiongroup gets its own database, so committed data never leaks into other groups and no manual cleanup orwithServerpodis needed; clean up in--concurrency=1only if later tests in the same group need a clean slate.tearDown
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 , use and ignore the endpoints parameter:
SessionwithServerpoddart
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>()),
);
});
});如果业务逻辑位于端点之外但需要,可使用并忽略endpoints参数:
SessionwithServerpoddart
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 to ensure a generator executes up to its before asserting:
flushEventQueue()yielddart
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()yielddart
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(default),afterEach, orafterAll.disabled - — apply pending migrations on start,
applyMigrationsby default.true - —
runModeby default, soServerpodRunMode.testis loaded.config/test.yaml - — adjust the loaded config, e.g. to point at another database.
configOverride
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.mdRunning tests
运行测试
bash
dart test # All tests
dart test -t integration # Only integration tests
dart test -x integration # Only unit testsNothing has to be started first — do NOT run . boots the server in the run mode, and the test database comes with it: projects created by use an embedded PostgreSQL that Serverpod launches and manages, or a SQLite file. A generated is present even in those projects, so seeing that file is not a reason to start Docker.
docker compose upwithServerpodtestserverpod createdocker-compose.yamlOnly when a run actually fails to reach the database is it worth opening : a section with / and no points at an external database, which does have to be running ( starts the one the project ships). With or set, fix the configuration or runtime instead — starting Docker will not help.
config/test.yamldatabasehostportdataPathdocker compose up -ddataPathfilePathbash
dart test # 所有测试
dart test -t integration # 仅运行集成测试
dart test -x integration # 仅运行单元测试无需提前启动任何服务——不要运行。 会在运行模式下启动服务器,测试数据库会随服务器一同启动:通过创建的项目会使用Serverpod启动和管理的嵌入式PostgreSQL,或SQLite文件。即使这些项目中存在生成的文件,也无需启动Docker。
docker compose upwithServerpodtestserverpod createdocker-compose.yaml只有当测试确实无法连接到数据库时,才需要查看:如果部分包含/且没有,则指向外部数据库,此时需要启动该数据库(可启动项目自带的数据库)。如果设置了或,则应修复配置或运行环境——启动Docker无法解决问题。
config/test.yamldatabasehostportdataPathdocker compose up -ddataPathfilePathProject structure
项目结构
Keep tests organized:
- - unit tests (no Serverpod dependency)
test/unit/ - - tests using
test/integration/withServerpod
Always call endpoints via the parameter, not by instantiating endpoint classes directly - the test tools handle lifecycle and validation to match production behavior.
endpoints保持测试结构清晰:
- - 单元测试(无Serverpod依赖)
test/unit/ - - 使用
test/integration/的测试withServerpod
始终通过参数调用端点,而非直接实例化端点类——测试工具会处理生命周期和验证,以匹配生产环境的行为。
endpoints