/serverpod:test — 백엔드 자동 검사지 만들기#
| 항목 | 내용 |
|---|---|
| 실행 명령 | /serverpod:test |
| 별칭 | /backend:test, /api:test |
| 모델 | sonnet |
| 사용 도구 |
Read
,
Edit
,
Write
,
Glob
,
Grep
,
Bash
|
| 연계 스킬 | serverpod, test |
Serverpod 백엔드(서버 쪽 코드)가 제대로 작동하는지 확인하는 "자동 검사지(테스트 코드)"를 대신 작성해 주는 도우미입니다.
한마디로#
새로 만든 서버 기능이 의도대로 동작하는지 매번 손으로 확인하지 않아도 되게, 자동으로 돌려보는 검사지(테스트 코드) 를 만들어 줍니다. 새 제품을 출고하기 전에 품질 검사 체크리스트를 자동으로 작성해 주는 것과 같아요.
누가·언제 쓰나요#
- 서버에 새 기능(엔드포인트)을 만든 뒤, 그게 잘 돌아가는지 검증하는 테스트가 필요할 때
/cc-flutter:feature:create(전체 기능 만들기) 과정 3단계에서 자동으로 호출됩니다- 로그인이 필요한 기능, 관리자만 쓸 수 있는 기능이 제대로 막혀 있는지 확인하고 싶을 때
무엇을 해주나요#
테스트 코드 파일을 자동으로 만들어 줍니다. 실제로 생기는 파일은:
-
backend/kobic_server/test/integration/{기능이름}_endpoint_test.dart— 통합 테스트 (실제 서버를 띄워 전체 흐름 확인) backend/kobic_server/test/unit/{기능이름}/{기능이름}_service_test.dart— 단위 테스트 (개별 로직 확인)
검사지에는 보통 이런 항목이 담깁니다: 로그인 없이 호출하면 막히는지, 권한 없는 사용자는 거부되는지, 만들기·읽기·수정·삭제(CRUD)가 정상 동작하는지, 잘못된 값을 넣으면 오류가 나는지 등.
어떻게 쓰나요#
# 기본 (통합 테스트 생성)
/serverpod:test
# 같은 기능을 하는 다른 호출 이름(별칭)
/backend:test
/api:test
테스트 종류는 integration(통합), unit(단위), both(둘 다) 중에서 고를 수 있고, 별도로 지정하지 않으면 통합 테스트가 만들어집니다.
만들어진 테스트를 실제로 돌려볼 때는:
# 통합 테스트만 실행
cd backend/kobic_server
dart test test/integration/
# 전체 테스트 실행
dart test
안에서 무슨 일이 벌어지나요#
- 새로 만든 서버 기능(엔드포인트) 코드를 살펴보고, 어떤 항목을 검사해야 할지 정리합니다.
- 로그인이 필요한 기능이면 "로그인 안 하면 막히는지" 검사를 추가합니다.
- 관리자 권한이 필요한 기능이면 "일반 사용자는 거부되는지" 검사를 추가합니다.
- 만들기·읽기·수정·삭제(CRUD)가 제대로 되는지, 잘못된 입력에 오류가 나는지 검사를 더합니다.
- 이 모든 항목을 하나의 테스트 파일로 만들어 줍니다.
⚙️ 상세 옵션·실행 명세 (개발자 / AI 에이전트용)
Role#
Generates test code for the Serverpod backend.
- Integration tests (using
withServerpodhelper) - Unit tests (service logic)
- Authentication/authorization tests
- Error case tests
Activation Conditions#
- Activated on
/serverpod:testcommand invocation - Called in Step 3 of
/cc-flutter:feature:createorchestration - Called when writing tests after endpoint creation
Parameters#
| Parameter | Required | Description |
|---|---|---|
feature_name | ✅ | Feature module name (snake_case) |
endpoint_name | ✅ | Endpoint class name (PascalCase) |
test_type | ❌ | integration, unit, both (default: integration) |
methods | ❌ | List of methods to test (default: all) |
Generated Files#
backend/kobic_server/test/
├── integration/
│ └── {feature_name}_endpoint_test.dart # Integration tests
└── unit/
└── {feature_name}/
└── {feature_name}_service_test.dart # Unit testsImport Order (Required)#
// 1. Test framework
import 'package:test/test.dart';
// 2. Generated protocol (models) - if needed
import 'package:kobic_server/src/generated/protocol.dart';
// 3. Test tools (auto-generated)
import 'test_tools/serverpod_test_tools.dart';
Core Patterns#
1. Integration Test Basic Structure#
import 'package:test/test.dart';
import 'package:kobic_server/src/generated/protocol.dart';
import 'test_tools/serverpod_test_tools.dart';
void main() {
withServerpod('Given {Feature} endpoint', (sessionBuilder, endpoints) {
// Test groups...
});
}
2. Unauthenticated Call Test#
group('Authentication required tests', () {
test('Throws ServerpodUnauthenticatedException when called without auth', () {
expect(
() => endpoints.{feature}.{method}(sessionBuilder),
throwsA(isA<ServerpodUnauthenticatedException>()),
);
});
});
3. Authenticated User Test#
group('Authenticated user tests', () {
late TestSessionBuilder authenticatedBuilder;
setUp(() {
authenticatedBuilder = sessionBuilder.copyWith(
authentication: AuthenticationOverride.authenticationInfo(
'1', // userId
{}, // scopes (empty Set: regular user)
),
);
});
test('{Entity} creation succeeds', () async {
// Arrange
final request = Create{Entity}Request(
title: 'Test Title',
description: 'Test Description',
);
// Act
final result = await endpoints.{feature}.create{Entity}(
authenticatedBuilder,
request,
);
// Assert
expect(result.id, isNotNull);
expect(result.title, equals('Test Title'));
});
});
4. Admin Permission Test (Console Endpoint)#
group('Admin permission tests', () {
late TestSessionBuilder adminBuilder;
setUp(() {
adminBuilder = sessionBuilder.copyWith(
authentication: AuthenticationOverride.authenticationInfo(
'1', // userId
{Scope.admin}, // Admin scope
),
);
});
test('List {Entity}s as admin succeeds', () async {
// Arrange
const limit = 10;
const offset = 0;
// Act
final result = await endpoints.{feature}Console.get{Entities}(
adminBuilder,
limit: limit,
offset: offset,
);
// Assert
expect(result, isA<List<{Entity}>>());
});
test('Regular user calling admin API throws permission error', () {
final userBuilder = sessionBuilder.copyWith(
authentication: AuthenticationOverride.authenticationInfo('1', {}),
);
expect(
() => endpoints.{feature}Console.get{Entities}(
userBuilder,
limit: 10,
offset: 0,
),
throwsA(isA<ServerpodInsufficientAccessException>()),
);
});
});
5. Error Case Tests#
group('Error cases', () {
late TestSessionBuilder authenticatedBuilder;
setUp(() {
authenticatedBuilder = sessionBuilder.copyWith(
authentication: AuthenticationOverride.authenticationInfo('1', {}),
);
});
test('Throws NotFoundException when querying non-existent ID', () {
expect(
() => endpoints.{feature}.get{Entity}(
authenticatedBuilder,
99999, // Non-existent ID
),
throwsA(isA<{Feature}NotFoundException>()),
);
});
test('Throws ValidationException with invalid parameters', () {
expect(
() => endpoints.{feature}.get{Entities}(
authenticatedBuilder,
limit: -1, // Invalid value
offset: 0,
),
throwsA(isA<Invalid{Feature}ParameterException>()),
);
});
});
6. Full CRUD Test (Create → Read → Update → Delete)#
group('Full CRUD flow', () {
late TestSessionBuilder adminBuilder;
setUp(() {
adminBuilder = sessionBuilder.copyWith(
authentication: AuthenticationOverride.authenticationInfo('1', {
Scope.admin,
}),
);
});
test('Create → Read → Update → Delete full flow', () async {
// 1. Create
final created = await endpoints.{feature}Console.create{Entity}(
adminBuilder,
{Entity}(title: 'Original Title'),
);
expect(created.id, isNotNull);
// 2. Read
final fetched = await endpoints.{feature}.get{Entity}(
adminBuilder,
created.id!,
);
expect(fetched.title, equals('Original Title'));
// 3. Update
final updated = await endpoints.{feature}Console.update{Entity}(
adminBuilder,
created.id!,
{Entity}UpdateRequest(title: 'Updated Title'),
);
expect(updated.title, equals('Updated Title'));
// 4. Delete
final deleted = await endpoints.{feature}Console.delete{Entity}(
adminBuilder,
created.id!,
);
expect(deleted, isTrue);
// 5. Verify deletion
expect(
() => endpoints.{feature}.get{Entity}(adminBuilder, created.id!),
throwsA(isA<{Feature}NotFoundException>()),
);
});
});
Unit Test Pattern#
Service Logic Test#
import 'package:test/test.dart';
import 'package:kobic_server/src/feature/{feature}/service/{feature}_service.dart';
import '../integration/test_tools/serverpod_test_tools.dart';
void main() {
withServerpod('Given {Feature}Service', (sessionBuilder, _) {
test('Business logic validation', () async {
// Arrange
final session = await sessionBuilder.build();
// Act
final result = await {Feature}Service.someBusinessLogic(
session,
param1: 'value1',
);
// Assert
expect(result, isNotNull);
});
});
}
Test Execution Commands#
Serverpod 4.0 local full-stack note: With Serverpod 4.0 (pinned
4.0.0-beta.0, beta — not GA), the bundled embedded Postgres lets integration tests run fully locally without Docker. Runserverpod start(which boots the backend server + embedded Postgres + Flutter app with sub-second hot reload), then rundart test test/integration/(ordart test -t integration) directly. Nodocker compose up -dneeded. See serverpod-local-fullstack. Thedocker compose up -dpath below is preserved as the v3 / legacy flow (kobic production is on 3.x).
# (3.x / legacy) Start Docker (PostgreSQL, Redis required)
docker compose up -d
# Run integration tests only
cd backend/kobic_server
dart test test/integration/
# Run specific file
dart test test/integration/{feature}_endpoint_test.dart
# Run all tests
dart test
# Test coverage
dart test --coverage=coverageTest Generation Flow#
flowchart TD
A[Analyze endpoint code] -- > B[Identify test cases]
B -- > C{Auth required?}
C -- > |Yes| D[Add auth tests]
C -- > |No| E[Public API tests]
D -- > F{Permissions required?}
F -- > |Yes| G[Add permission tests]
F -- > |No| H[Regular user tests]
G -- > I[Add CRUD tests]
H -- > I
E -- > I
I -- > J[Add error cases]
J -- > K[Generate test file]Test Case Checklist#
Required Test Cases#
| Case | Description |
|---|---|
| Unauthenticated call | Verify ServerpodUnauthenticatedException is thrown |
| Insufficient permissions | Verify ServerpodInsufficientAccessException is thrown |
| Normal CRUD | Verify Create, Read, Update, Delete success |
| Lookup failure | Verify NotFoundException is thrown |
| Validation error | Verify ValidationException is thrown |
| Paging validation | Verify limit, offset parameter behavior |
Additional Test Cases (Optional)#
| Case | Description |
|---|---|
| Transaction rollback | Verify DB changes are rolled back |
| Concurrency | Verify concurrent request handling |
| Cache behavior | Verify Redis cache application |
| External service integration | Use Mock/Stub |
withServerpod Options#
withServerpod(
'Given {Feature} endpoint',
(sessionBuilder, endpoints) {
// Tests...
},
runMode: ServerpodRunMode.production, // Default: development
enableSessionLogging: false, // Default: true (development)
rollbackDatabase: RollbackDatabase.afterEach, // Default: afterEach
);
RollbackDatabase Options#
| Option | Description |
|---|---|
afterEach | Rollback after each test (default, recommended) |
afterAll | Rollback after all tests |
disabled | Disable rollback (use with caution) |
Checklist#
- Follow import order
- Use
withServerpodhelper - Include authentication tests
- Include permission tests (Console endpoints)
- Include error case tests
- Follow Arrange-Act-Assert pattern
- Clearly separate test groups
- Use descriptive test descriptions