LogoSkills

serverpod-test-agent

Serverpod 테스트 전문가. Integration/Unit 테스트 생성에 사용합니다.

/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

안에서 무슨 일이 벌어지나요#

  1. 새로 만든 서버 기능(엔드포인트) 코드를 살펴보고, 어떤 항목을 검사해야 할지 정리합니다.
  2. 로그인이 필요한 기능이면 "로그인 안 하면 막히는지" 검사를 추가합니다.
  3. 관리자 권한이 필요한 기능이면 "일반 사용자는 거부되는지" 검사를 추가합니다.
  4. 만들기·읽기·수정·삭제(CRUD)가 제대로 되는지, 잘못된 입력에 오류가 나는지 검사를 더합니다.
  5. 이 모든 항목을 하나의 테스트 파일로 만들어 줍니다.

⚙️ 상세 옵션·실행 명세 (개발자 / AI 에이전트용)

Role#

Generates test code for the Serverpod backend.

  • Integration tests (using withServerpod helper)
  • Unit tests (service logic)
  • Authentication/authorization tests
  • Error case tests

Activation Conditions#

  • Activated on /serverpod:test command invocation
  • Called in Step 3 of /cc-flutter:feature:create orchestration
  • Called when writing tests after endpoint creation

Parameters#

ParameterRequiredDescription
feature_nameFeature module name (snake_case)
endpoint_nameEndpoint class name (PascalCase)
test_typeintegration, unit, both (default: integration)
methodsList 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 tests

Import 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. Run serverpod start (which boots the backend server + embedded Postgres + Flutter app with sub-second hot reload), then run dart test test/integration/ (or dart test -t integration) directly. No docker compose up -d needed. See serverpod-local-fullstack. The docker compose up -d path 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=coverage

Test 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#

CaseDescription
Unauthenticated callVerify ServerpodUnauthenticatedException is thrown
Insufficient permissionsVerify ServerpodInsufficientAccessException is thrown
Normal CRUDVerify Create, Read, Update, Delete success
Lookup failureVerify NotFoundException is thrown
Validation errorVerify ValidationException is thrown
Paging validationVerify limit, offset parameter behavior

Additional Test Cases (Optional)#

CaseDescription
Transaction rollbackVerify DB changes are rolled back
ConcurrencyVerify concurrent request handling
Cache behaviorVerify Redis cache application
External service integrationUse 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#

OptionDescription
afterEachRollback after each test (default, recommended)
afterAllRollback after all tests
disabledDisable rollback (use with caution)

Checklist#

  • Follow import order
  • Use withServerpod helper
  • 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