关于 serverpod-testing
serverpod-testing 是 Serverpod 测试技能,面向需要验证端点和业务逻辑的 Dart 开发者。它介绍带服务器上下文的测试、数据库回滚、身份验证及流测试。
主要功能
- 01使用生成的测试工具调用端点
- 02配置会话与身份验证
- 03控制测试数据库回滚
- 04测试流与带 Session 的业务逻辑
适用场景
- 01验证端点的集成行为
- 02测试需要数据库会话的业务逻辑
- 03隔离测试数据并检查流事件
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ serverpod/ serverpod --skill serverpod-testing安装后在 Claude Code 中输入/serverpod-testing确认已加载。
附带文件references/with-serverpod-options.md
详细介绍
Serverpod 测试
生成的测试工具可让你在测试中调用端点,并使用完整的服务端上下文(数据库、缓存等)。请导入生成的测试工具文件,而不是直接导入 serverpod_test——该文件会重新导出所需的所有内容。导入路径来自 config/generator.yaml(server_test_tools_path);以下示例使用常见的 test_tools/serverpod_test_tools.dart 输出路径。
优先使用 Given/when/then 描述。嵌套分组与测试名称合起来,应包含一个清晰的 Given、一个 when 和一个 then,这样无需阅读代码也能理解失败原因。
基本测试
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');
});
});
}
会话构建器
使用 sessionBuilder.copyWith(...) 创建修改后的会话。调用 sessionBuilder.build() 获取用于数据库操作或传递给辅助函数的 Session。
已通过身份验证的测试
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));
});
});
无需手动设置 tearDown——默认情况下,每项测试都在一个会回滚的事务中运行。
回滚行为
默认值:RollbackDatabase.afterEach——每项测试都在回滚事务中运行。
afterAll——在分组中的所有测试运行完毕后回滚。适用于连续测试相互依赖且设置开销较大的场景测试。disabled——不自动回滚。当端点代码使用并发的session.db.transaction(...)调用时必须使用(嵌套事务会抛出InvalidConfigurationException)。每个withServerpod分组都有自己的数据库,因此已提交的数据不会泄漏到其他分组,也无需手动清理或使用--concurrency=1;仅当同一分组中的后续测试需要干净的初始状态时,才在tearDown中清理。
withServerpod(
'Given concurrent transactions',
(sessionBuilder, endpoints) {
test('then should commit all', () async {
await endpoints.products.concurrentTransactionCalls(sessionBuilder);
});
},
rollbackDatabase: RollbackDatabase.disabled,
);
使用 Session 测试业务逻辑
如果逻辑位于端点之外,但需要 Session,请使用 withServerpod 并忽略 endpoints 参数:
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>()),
);
});
});
测试流
使用 flushEventQueue() 确保生成器在断言前执行到 yield:
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 选项
以下选项最常改变行为:
rollbackDatabase—afterEach(默认值)、afterAll或disabled。ephemeralDatabase— 每个分组使用独立的空数据库,默认值为true。设为false可使用已配置的数据库,包括先前已填充数据的数据库。该数据库不会被创建或删除,且共享该数据库的分组不能并行运行。applyMigrations— 启动时应用待处理的迁移,默认值为true。runMode— 默认值为ServerpodRunMode.test,因此会加载config/test.yaml。configOverride— 调整已加载的配置,例如指向另一个数据库。
包含默认值的完整列表请参见 references/with-serverpod-options.md。
运行测试
dart test # All tests
dart test -t integration # Only integration tests
dart test -x integration # Only unit tests
无需预先启动任何内容——不要运行 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 无济于事。
项目结构
请合理组织测试:
test/unit/— 单元测试(不依赖 Serverpod)test/integration/— 使用withServerpod的测试
始终通过 endpoints 参数调用端点,不要直接实例化端点类——测试工具会处理生命周期和验证,使行为与生产环境一致。