mcpField
S
serverpod-testing

serverpod-testing

github.com/serverpod测试与质量3.3k45 次安装1 天前更新
代码仓库

关于 serverpod-testing

serverpod-testing 是 Serverpod 测试技能,面向需要验证端点和业务逻辑的 Dart 开发者。它介绍带服务器上下文的测试、数据库回滚、身份验证及流测试。

主要功能

  1. 01使用生成的测试工具调用端点
  2. 02配置会话与身份验证
  3. 03控制测试数据库回滚
  4. 04测试流与带 Session 的业务逻辑

适用场景

  1. 01验证端点的集成行为
  2. 02测试需要数据库会话的业务逻辑
  3. 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 参数调用端点,不要直接实例化端点类——测试工具会处理生命周期和验证,使行为与生产环境一致。

评论

支持 Markdown