규칙

테스트

언어와 상관없이 같은 규칙 (ADR-0003). 공통 설정 · 실제 서버 띄우기 · Fake 넣기 · 격리 방법은 그 서버의 AGENTS.md. 왜 이런 규칙인지, 테스트 하나를 줄마다 읽는 예는 concepts/testing.md.

실행 경계

기본은 서비스 범위의 API 통합 테스트다. 실제 서버를 랜덤 포트로 띄우고 HTTP · gRPC 로 요청한다. api · domain · storage 는 실제 구현을 쓰고, 외부 클라이언트(clients)만 Fake 로 바꾼다.

API 통합 테스트
storage테스트 DB실제 Postgres
clientsFake외부 시스템 대역
1 / 5

테스트가 실제 서버에 HTTP 또는 gRPC 요청을 보냅니다. 서버는 랜덤 포트에서 실행합니다.

실제 외부 서비스 · 브라우저 · 프록시는 이 테스트 범위 밖입니다.

  • 테스트 DB 는 운영과 같은 엔진, docker/docker-compose.middleware.yaml 의 Postgres 에 서버마다 <서버>_test 다. 먼저 pnpm infra (루트 pnpm test · CI 는 스스로 띄운다). 개발 DB <서버> 는 건드리지 않는다.
  • 인증 · 브라우저 · 프록시(vite · next) · 실제 외부 서비스는 이 테스트 범위 밖이다. 필요하면 따로 둔다 (브라우저까지는 verify-local skill).
  • 좁은 계층(예: 스트림 전송)만 검사할 때는 domain 을 대역으로 바꿀 수 있지만, 결과도 그 범위로만 해석한다.
  • gateway 는 업무 · DB 가 없다. 실제 gateway 를 랜덤 포트로 띄우고 뒤의 서버를 Fake HTTP 서버로 바꿔, 요청이 맞는 서버로 그대로 가고 없는 경로는 404, 닿지 못하면 502 인지 본다. 서버의 자원이 없으니 파일은 경로 표 하나(gateway_specs_test.go)다.

인프라 기반 확인

테스트가 기대는 인프라는 docker/ 의 compose 한 벌이다. 개발 · 테스트 · CI 가 같은 파일로 띄운다.

층 띄우는 것 돌리는 곳
서비스 테스트 ({METHOD}_specs) 서버 프로세스 + Postgres (docker-compose.middleware.yaml) pnpm test, CI
전체 스택 (docker/smoke.sh) 서버 · 웹 앱 이미지 + compose 네트워크 + Postgres (docker-compose.yaml) pnpm stack (로컬)
  • 경우마다의 동작(성공 · 400 · 502 · 저장 상태)은 서비스 테스트가 본다. 전체 스택은 이미지 · 네트워크 · 환경 변수가 이어지는지만 본다: 메모 하나를 만들어 notes → tags(태그) → activity(기록) gRPC 흐름이 실제로 이어지는지, gateway 가 경로마다 맞는 서버로 넘기는지, 웹 앱은 화면 · /api 프록시 · site 서버 렌더링. 서버끼리 부르는 흐름이 새로 생기면 그 흐름 하나를 smoke 에 더한다.
  • 인프라(캐시 · 큐 · 스토리지)를 더하면 docker-compose.middleware.yaml 에 서비스를 더하고, 테스트용 자원(DB · 버킷 이름)을 서버마다 따로 둔다. 실제 외부 서비스(결제 · 메일)는 인프라가 아니라 Fake 다.

이름

  • 디렉터리 = 리소스 경로, 파일 = {METHOD}_specs (대문자). Path 변수는 하위 디렉터리: /api/notes → notes/GET_specs, /api/notes/{id}/tags → notes/id/tags/GET_specs
  • 묶음 이름은 메서드 + 엔드포인트: GET /api/notes/{id}.
  • gRPC 는 grpc/{Service}/{Rpc}_specs, 묶음 이름은 gRPC 경로 /app.tag.v1.TagService/Suggest. 상태 코드 자리에 gRPC status 를 쓴다: action이_비었으면_INVALID_ARGUMENT를_반환한다.
  • 테스트 이름은 한글_밑줄로 조건과 기대 결과: 올바른_메모를_보내면_200_OK를_반환하고_DB에_저장된다. 상태 코드가 핵심이면 코드명을, 저장 결과가 핵심이면 저장 결과를 넣는다.
  • 기존 API 의 새 사례는 그 *_specs 에 추가. 순수 함수 단위 테스트는 목적이 드러나는 이름으로 따로.
  • 준비 코드는 {Domain}Fixture, 외부 대역은 Fake{Name} 처럼 역할을 이름에 드러낸다. 다른 서버의 gRPC 클라이언트 대역도 같다: notes 의 FakeTagService · FakeActivityService.

본문

  • Arrange → Act → Assert. 한 테스트는 시나리오 하나, assertion 은 여러 개여도 된다.
  • HTTP 응답 · 저장 상태 · 필요한 부수 효과를 함께 검사한다. 저장 상태는 storage 의 저장소로 확인한다 (테스트에 스키마 SQL 을 다시 쓰지 않는다).
  • 오류 응답은 문자열 조각이 아니라 JSON 필드로 비교한다 ({ error: '...' }).
  • 호출 횟수는 중복 방지 · 재시도처럼 횟수 자체가 계약일 때만 검사한다.

Fake

  • 외부 클라이언트는 교체 지점을 하나 둔다. 실제 구현은 만들 때 외부에 접속하지 않게(처음 호출할 때 연결) 한다.
  • mock(@MockitoBean, mock.module, patch)으로 바꾸지 않는다. hook 이 막는다.
  • Fake 는 테스트 공통 설정 옆에 두고, 공통 설정에서 한 번 등록한다.
  • Fake 는 외부 조건만 통제한다: 반환값 · 다음 호출 실패(failNext()) · 받은 인자 기록 · 완료 보류. 업무 정책이나 재시도를 Fake 안에 다시 구현하지 않는다. 재시도 검증은 “첫 호출 실패, 다음 성공” 으로 설정하고 재시도는 실제 domain 이 하게 한다.
  • Fake 의 실패(예: 연결 실패)를 API 오류 계약(400/502)으로 바꾸는 건 실제 domain · 오류 처리다. 그 연결이 검증 대상이다. notes 의 예: fakeTags.failNext() 면 502 이고 저장되지 않는다, fakeActivity.failNext() 면 그래도 201 이다.
  • 상태(실패 플래그 · 카운터 · 마지막 인자 · 기록)는 reset() 으로 비우고 공통 설정의 테스트 전후에 부른다.
  • 메모리 기록은 실제 발송 성공의 근거가 아니다. 클라이언트 자체(요청 형식 · 인증 · 오류)는 실제 서버나 계약을 재현한 테스트 서버로 따로 검증한다.

격리

  • 같은 Fake · DB 를 쓰는 테스트는 병렬로 돌리지 않는다 (한 테스트의 reset 이 다른 테스트를 지운다).
  • 서버 · 컨텍스트를 테스트끼리 같이 쓰면 DB · Fake 를 테스트마다 비운다. 서버는 다른 스레드라 rollback 을 가정하지 않는다.
  • 비울 때 마이그레이션 기록 표(flyway_schema_history · pgmigrations · alembic_version)는 남긴다. TRUNCATE 는 id 를 되돌리지 않으니 id 는 응답 · 저장소가 준 값을 쓴다.
  • 서버마다 테스트 DB 가 달라서 서버끼리는 같이 돌아도 된다 (pnpm -r test).

스트리밍 (SSE 를 추가하면)

응답을 다 받은 뒤 data: 를 검사하면 서버가 모아서 보내도 통과한다. 이벤트를 받은 시점에 서버 쪽 작업이 아직 끝나지 않았는지 확인한다 (완료를 latch · Promise 로 보류했다가 검사 후 풀기, 고정 sleep 금지, 대기 상한 둠). X-Accel-Buffering: no 헤더가 있다고 실제 프록시의 버퍼링까지 검증된 건 아니다.

GitHub 에서 고치기