가입·로그인 API 테스트
110분 안팎
학습 목표
성공·중복·오류·미인증·타인 접근을 자동 검증합니다.
개념
한 번의 호출을 반복 가능한 API 검사로 바꿉니다
QA가 매번 가입하고 쿠키를 복사하고 프로필을 확인하면 입력 누락과 실행 순서 차이가 섞이기 쉽습니다. 핵심 흐름은 자동 요청으로 반복하고 사람은 실패 근거와 범위 판단에 시간을 씁니다. 이번 레슨은 node:test의 async 테스트에서 fetch로 요청을 보내고 HTTP 상태·본문·권한 계약을 확인합니다. 성공 가입뿐 아니라 중복, 형식 오류, 미인증, 틀린 비밀번호, 타인 접근을 다른 기대 결과로 명시합니다.
qa-api-suite의 api/suite.test.cjs에는 요청과 시나리오가 준비되어 있습니다. 학습자는 api/validate.cjs의 validateHttp를 완성합니다. 함수는 response와 독립 expected를 받고 맞으면 true, 다르면 AssertionError를 던집니다. HTTP 응답이 기대와 다를 때 false만 반환하면 제공 assert.throws 검사가 계약대로 동작하지 않습니다. checks/response.cjs의 validateResponse를 재사용하여 앞 모듈의 타입·필수값·판정·회원 수 단언을 연결합니다.
요청의 완료와 검증의 완료를 잇습니다
const r = await fetch(...)는 응답 헤더를 받은 결과를 반환합니다. 이어 await r.json()으로 본문 읽기와 JSON 파싱까지 기다립니다. fetch가 반환한 Response 자체를 JSON 본문으로 넘기면 검증 대상이 틀립니다. 본문 읽기도 비동기 단계이며 같은 스트림을 다시 읽으려 하지 않습니다. 요청 함수가 파싱 결과와 상태·Content-Type·requestId·쿠키 전달 자료를 한 객체에 담아 검증 함수를 호출하기 쉽게 만듭니다.
HTTP 400이나 403도 Response로 받을 수 있으므로 catch만 두고 오류 응답을 분류하지 않습니다. 연결 실패나 시간 제한 취소는 요청 Promise의 거절이고, 기대 상태와 실제 상태가 다른 경우는 단언 실패입니다. 정상 중복 가입의 409는 이 테스트에서 기대한 제품 동작입니다. 모든 4xx를 예외로 바꾸는 공용 함수는 이 사례의 거절 본문 검증을 방해할 수 있어 요청 수집과 계약 판정을 분리합니다.
signal: AbortSignal.timeout(3000)은 기다림에 상한을 둡니다. 이는 제품 성능 요구사항이 3초라는 합의가 아니라 실습이 무한 대기하지 않게 한 장치입니다. 제한을 넘으면 실제 응답을 관찰하지 못했으므로 가입 성공으로 처리하지 않습니다. 환경이 느리다는 이유로 단언을 삭제하는 대신 실행 환경과 실패 위치를 확인합니다. 요청에 성공했어도 JSON 파싱이 실패하면 Content-Type과 원래 응답 종류를 확인합니다.
로그인 세션을 후속 요청에 명시합니다
이 제공 앱은 로그인 성공 시 JSESSIONID 쿠키를 설정합니다. request는 set-cookie 헤더에서 첫 세미콜론 앞 name=value를 구해 다음 요청의 Cookie 헤더에 넣습니다. Node 요청이 브라우저처럼 쿠키를 자동 유지한다고 가정하지 않습니다. 이번 앱은 로그인에 쿠키 하나를 사용하므로 가능한 단순 처리이며 여러 Set-Cookie를 가진 제품에 그대로 확대하지 않습니다. 일반 쿠키 저장소를 구현하는 과제는 이번 범위가 아닙니다.
401을 확인하는 익명 조회에는 의도적으로 쿠키를 넣지 않습니다. 본인 조회에는 성공 로그인에서 받은 쿠키를 전달합니다. A 세션으로 B 프로필을 요청하면 403과 개인정보 미노출을 기대합니다. 타인 수정 요청 역시 403이며 B가 바뀌지 않는지는 다음 저장 레슨에서 확인합니다. 요청 조건을 함수 인자로 드러내면 인증 쿠키를 잘못 재사용해 익명 테스트가 통과하는 상황을 리뷰에서 발견하기 쉽습니다.
시나리오의 준비·실행·단언을 읽습니다
API-01은 flow@example.test의 가입을 먼저 완료하고 중복·형식 오류·미인증을 확인합니다. 틀린 비밀번호 로그인 뒤에도 본인 조회가 허용되지 않아야 합니다. 올바른 로그인 후 본인 닉네임을 변경하고 다시 조회합니다. 재조회는 응답의 새이름을 다시 읽는 것보다 한 단계 더 관찰하지만, 저장 계층의 실제 행 변경을 완전히 증명하는 것은 아닙니다. 저장소 단언의 책임을 API 단언과 구분합니다.
API-02는 다른 회원을 추가한 다음 A 세션으로 타인 조회와 수정을 거절하는지 확인합니다. 이어 wrong-body와 foreign-read라는 실습 전용 결함 주입으로 서버가 실제로 잘못된 응답을 만들게 합니다. 숫자 nickname의 200 응답과 타인 조회 허용 응답이 validateHttp에서 거절되면 검출 테스트가 통과합니다. 이 헤더는 합성 로컬 앱에만 존재하며 운영 제품에 결함 주입을 시도하는 절차가 아닙니다.
API-03은 네트워크 결과와 별개로 검사 함수 자체의 민감도를 확인합니다. 정상 객체의 상태, Content-Type, count 타입, result, requestId, 비밀 필드를 하나씩 바꿉니다. 같은 검증 함수가 이러한 변형을 모두 거절해야 합니다. 정상 흐름만 통과하는 함수와 결함을 실제로 찾는 함수의 차이를 보여 줍니다. assert.throws의 통과는 제품이 바르게 응답했다는 뜻이 아니라 주어진 잘못된 응답을 검출했다는 뜻입니다.
판정 함수에 요청 맥락을 연결합니다
validateHttp는 먼저 status를 expected.status와 비교하고 JSON Content-Type인지 확인합니다. 그 다음 validateResponse(response.body, expected)를 호출합니다. expected의 result와 count는 인수 기준에서 정한 값입니다. requestId는 이 요청에 실어 보낸 값과 정확히 같아야 합니다. 단지 공백 아닌 문자열만 검사하면 다른 요청의 결과가 섞여도 놓칠 수 있습니다. 요청과 응답을 묶는 단언을 기존 본문 검사 위에 추가합니다.
expected.email이 있을 때는 본인 프로필의 email·nickname을 확인합니다. 401·403일 때는 응답에 email·nickname 필드가 없어야 하며 모든 응답에서 password·secret 필드가 없어야 합니다. 어떤 성공 응답에도 같은 프로필 필드를 강요하지 않습니다. 로그인 성공 응답의 목적은 인증 완료와 세션 설정이고 프로필 조회 응답의 목적은 본인 정보를 전달하는 것이므로 유형에 따라 관찰 대상이 다릅니다.
실행 환경 오류와 검증 실패를 구별합니다
작성 샌드박스는 로컬 포트 bind를 허용하지 않아 curl과 fetch의 실제 네트워크 실행은 외부 검증 대기입니다. 제공 bash check.sh는 외부 환경에서 Java 테스트가 서버를 띄우고 QA_BASE_URL을 Node에 전달하여 실행합니다. Node 파일만 직접 실행해 로컬 주소 필요라는 단언이 나왔다면 제품 결함이 아니라 제공 실행 절차를 빠뜨린 것입니다. 네트워크 검사가 대기 중인 사실을 통과로 기록하지 않습니다.
미션 기본 검사는 포트 대신 MockMvc를 사용합니다. 실제 컨트롤러와 H2를 거친 요청·응답 14개를 JSON으로 수집하여 같은 validateHttp에 넣습니다. 이것은 단순 손으로 만든 응답 fixture보다 넓은 통합 범위지만 DNS·TCP·쿠키의 실제 브라우저 동작을 확인하지 않습니다. 외부 환경에서는 api-app의 별도 네트워크 명령을 추가 실행합니다. in-process 검사의 통과와 실제 fetch의 실행 대기를 별도 증거로 남깁니다.
순서 의존성과 다음 과제를 남깁니다
제공 suite의 두 시나리오는 첫 시나리오가 만든 회원을 이어 사용합니다. 이번 단계는 그 의존을 명시하고 순차 실행합니다. API-02만 선택 실행하거나 공유 상태에 병렬 요청을 던지면 기대 count와 준비 데이터가 달라질 수 있습니다. API 호출 자동화가 끝났다고 데이터 격리가 완성된 것은 아닙니다. 다음 모듈에서 사례별 준비·정리와 독립 실행을 개선할 대상으로 기록합니다.
Missing expected exception은 검증기가 잘못된 응답을 그대로 수용했다는 신호입니다. Expected values to be strictly equal은 actual과 expected를 읽고 상태인지 본문인지 requestId인지 메시지로 좁힙니다. fetch failed는 그 이전 요청 단계의 문제일 수 있습니다. 완료 시 자동화한 흐름, 의도적으로 검출한 결함, 네트워크 실행 상태, UI 제외 범위를 기록하면 동료가 테스트 결과를 제품 전체의 품질 결론으로 과장하지 않게 됩니다.
Node.js 20 fetch 공식 문서에서 실습의 옵션과 API 정의를 확인할 수 있습니다.
따라하기
본문 검증 함수를 연결합니다
앞 모듈 함수를 호출하는 구조를 작은 예제로 실행합니다. 로컬 실습에서는 제공 checks/response.cjs의 더 엄격한 함수로 바꿉니다.
const assert=require('node:assert/strict');
function validateResponse(b,e){assert.equal(b.result,e.result);assert.equal(b.count,e.count);return true;}
function validateHttp(r,e){assert.equal(r.status,e.status);return validateResponse(r.body,e);}
console.log(validateHttp({status:201,body:{result:'VALID',count:1}},{status:201,result:'VALID',count:1}));실행 결과
true
다른 요청의 ID를 거절합니다
정상 문자열이라도 echo가 틀리면 단언이 실패합니다. 예외를 기대하는 검사도 정상 완료를 출력할 수 있습니다.
const assert=require('node:assert/strict');
assert.throws(()=>assert.equal('request-other','request-self'),{code:'ERR_ASSERTION'});
console.log('requestId mutant DETECTED');실행 결과
requestId mutant DETECTED
Promise 거절을 기다립니다
네트워크 대신 실패를 주입한 loader로 원본 예외 전달을 확인합니다. 실제 서버 응답을 관찰한 출력은 아닙니다.
const assert=require('node:assert/strict');
(async()=>{const failure=new Error('synthetic transport');
await assert.rejects(async()=>{throw failure;},e=>e===failure);
console.log('transport rejection DETECTED');})();실행 결과
transport rejection DETECTED
API 검사를 실행합니다
외부 환경에서 qa-api-suite의 api/validate.cjs를 본문의 계약대로 완성한 뒤 실행합니다. API-01·02·03 모두 실행되고 fail·cancelled·skipped·todo 0인지 확인합니다. 실제 fetch 결과는 작성 샌드박스에서 실행 대기입니다.
bash check.sh확인 문제
실습
api/validate.cjs의 validateHttp를 완성합니다. checks/response.cjs를 재사용하고 HTTP 상태·JSON 타입·requestId echo·본인 프로필·401/403 개인정보 미노출·비밀 필드 미노출을 단언합니다. 제공 시나리오와 변형 검사를 유지합니다. 실제 fetch 실행은 포트 제한으로 외부 검증 대기입니다.
실행 명령
bash check.sh
기대 결과
JUnit 1개 및 API-01·02·03 Node 3개 통과; fail/cancelled/skipped/todo 0; 외부 검증 대기
모범 답안
모범 답안 내려받기더 읽기
면접 질문
- API 응답 코드가 성공이어도 테스트가 실패할 수 있는 상황을 설명해 주시면 됩니다.