serverpod-testing について
serverpod-testing は、エンドポイントやビジネスロジックを検証する Dart 開発者向けの Serverpod テストガイドです。サーバーコンテキスト付きテスト、データベースのロールバック、認証、ストリームのテストを説明します。
主な機能
- 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 テスト
生成されたテストツールを使うと、サーバーの完全なコンテキスト(DB、キャッシュなど)を使って、テスト内でエンドポイントを呼び出せます。serverpod_test を直接インポートするのではなく、生成されたテストツールのファイルをインポートしてください。このファイルは必要なものをすべて再エクスポートしています。インポートパスは config/generator.yaml の server_test_tools_path で指定します。以下の例では、一般的な出力先である test_tools/serverpod_test_tools.dart を使用しています。
Given/when/then の記述を優先してください。ネストしたグループとテスト名を通して、コードを読まなくても失敗の原因を説明できるよう、明確な Given、when、then がそれぞれ 1 つずつ含まれるようにしてください。
基本的なテスト
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(...) を使って、変更したセッションを作成します。DB 操作やヘルパーへの受け渡しに使う Session を取得するには、sessionBuilder.build() を呼び出してください。
認証済みテスト
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 を使い、エンドポイントのパラメーターを無視してください。
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>()),
);
});
});
ストリームのテスト
アサーションを行う前にジェネレーターが yield まで実行されるよう、flushEventQueue() を使ってください。
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 パラメーターを通じてエンドポイントを呼び出してください。テストツールがライフサイクルとバリデーションを管理し、本番環境と同じ動作を再現します。