기대값과 검증 함수
85분 안팎
학습 목표
구현을 복사하지 않고 인수 기준을 검증합니다.
개념
검증기의 답은 제품 밖에서 정합니다
앞 모듈 TC-05는 ASCII 비밀번호 64자가 허용된다는 계약에 따라 가입 성공과 회원 수 증가를 기대했습니다. 결함 응답이 INVALID라고 왔다고 기대값을 INVALID로 바꾸면 같은 결함을 다시 찾을 수 없습니다. QA가 만드는 오라클은 관찰 결과를 정답으로 복사하는 장치가 아닙니다. 요구사항과 인수 기준에 근거해 예상 결과를 먼저 정하고 실제 결과를 비교하는 기준입니다.
TC-26은 스페이스 세 개 닉네임을 거절하고 새 회원을 만들지 않아야 합니다. 따라서 기대 판정 BLANK_NICKNAME과 기대 회원 수 0을 고정합니다. 두 사례 모두 HTTP 상태는 200일 수 있어 판정 필드와 저장 결과를 따로 읽습니다. 이 레슨의 비교 대상은 이미 기록한 결과이며 새 HTTP 요청은 보내지 않습니다. 네트워크 응답과 저장소 재조회는 다음 모듈에서 연결합니다.
입력·기대·실제·진단을 나눕니다
검증 함수는 expected와 actual을 별도 인자로 받습니다. expected에는 합의한 result와 count가 있고 actual에는 관찰한 응답 값이 있습니다. testId는 실패를 테스트 표로 되돌려 찾기 위한 이름입니다. actual.expected 또는 actual.matches를 읽어 최종 판정하지 않습니다. 그 값도 관찰 자료가 잘못 생성되면 틀릴 수 있으므로 검증 근거를 독립적으로 유지합니다.
함수는 같은 입력을 받으면 같은 결과를 내도록 만들고 외부 상태를 바꾸지 않습니다. 예를 들어 비교 함수가 회원 수를 증가시키거나 실제 객체에 expected 값을 덮어쓰면 검증 뒤의 데이터가 달라집니다. 원본 증거와 판정 로직을 분리하면 함수를 여러 사례에 재사용할 수 있습니다. 함수 이름에는 제품 기능 이름보다 무엇을 비교하는지를 드러내어 응답 검사와 가입 구현을 혼동하지 않습니다.
검사 순서는 먼저 형태, 다음 의미입니다. actual이 null이면 actual.result에 접근하기 전에 본문 실패를 돌려줍니다. result가 문자열이 아니면 기대 판정과의 값 비교보다 타입 오류를 우선 알립니다. count가 안전한 비음수 정수가 아니면 기대 회원 수와 같아 보여도 실패합니다. 여러 오류를 동시에 모을 수도 있지만 이번 실습은 첫 실패 하나를 정해 일관된 진단을 연습합니다.
기대값을 계산할 때 생기는 함정
제품이 닉네임을 검사하는 코드를 그대로 테스트에 가져오면 제품의 잘못된 조건도 함께 복제할 수 있습니다. 이번 검증기는 닉네임 검사 구현을 다시 만들지 않습니다. 이미 설계한 특정 입력의 인수 기준을 VALID·1 또는 BLANK_NICKNAME·0이라는 고정값으로 표현합니다. 이 값이 어디서 왔는지 TC ID와 요구사항 ID를 함께 남겨 팀이 검토할 수 있게 합니다.
독립 기대값이 항상 사람이 손으로 모든 조합을 작성해야 한다는 뜻은 아닙니다. 표로 합의한 사례를 데이터로 저장해 반복할 수 있습니다. 다만 기대값 생성 규칙과 실제 결과 생성 규칙이 같은 결함을 공유하지 않는지 검토합니다. 실제 관찰의 matches가 true라서 통과했다는 설명은 독립 근거가 아닙니다. 어떤 합의에서 expected를 정했는지를 설명할 수 있어야 합니다.
필드 전체를 동일하게 비교하면 요청마다 다른 requestId나 설명 문구 때문에 의미 있는 응답도 실패할 수 있습니다. 반대로 필요한 필드까지 빼면 저장 결과 오류를 놓칩니다. 이번 단계는 result와 count의 의미를 엄격히 비교하고 requestId는 형태를 확인합니다. message는 로컬 검사에서 공백 아닌 문자열인지 확인하지만 UI에 실제로 표시되었는지는 증명하지 않습니다. 검증 범위를 요구사항의 관찰 층과 연결합니다.
조건을 진단 메시지로 바꿉니다
브라우저 실습에서는 compare 함수가 testId 뒤에 PASS 또는 FAIL과 필드명을 붙입니다. result와 count가 모두 다르면 result를 먼저 보고합니다. 타입 불일치와 값 불일치를 result:type, result:value처럼 구별합니다. 단순히 false라고만 반환하는 것보다 어떤 규칙이 깨졌는지 바로 볼 수 있습니다. 실습 출력 계약은 정해진 진단 문자열이며 실제 시스템의 오류 문구와 구분합니다.
로컬에서는 node:assert/strict의 equal을 사용하면 엄격 비교가 실패할 때 AssertionError를 던집니다. 함수 인자는 실제 값, 기대값, 설명 순서입니다. assert.equal(actual.count, expected.count, 'count: acceptance mismatch')처럼 필드를 이름으로 표시합니다. expected와 actual을 뒤집으면 검사는 여전히 실패할 수 있지만 메시지에서 실제와 기대의 해석이 반대로 되어 조사 시간을 늘립니다.
객체 두 개에 ===를 사용하면 속성 내용이 아니라 같은 객체를 가리키는지 비교합니다. 내용이 같은 객체도 별도로 만들면 false입니다. 구조 전체를 비교할 때는 strict 모듈의 deepEqual을 사용합니다. 이번 가입 검사에서는 필드별 타입과 의미를 더 구체적으로 설명하려고 equal을 나누어 사용합니다. 모든 객체를 문자열로 바꿔 비교하는 방법은 키 순서나 불필요한 필드 차이에 묶일 수 있어 검증 의도를 먼저 정합니다.
통과와 거절을 함께 증명합니다
검증기가 잘못된 값을 거절하는지도 테스트해야 합니다. 정상 fixed 응답을 넣으면 통과하고 buggy 응답을 넣으면 AssertionError가 나야 합니다. 오류가 나기를 기대하는 테스트는 assert.throws에 함수 형태로 넘깁니다. 검증 함수를 미리 실행해 그 반환값을 넘기면 오류를 포착할 기회를 잃습니다. 실패를 기대한 테스트 자체의 통과는 검증기가 결함을 발견했다는 뜻입니다.
특히 result는 맞지만 count만 다른 변형을 만듭니다. 성공 판정 VALID와 count 0을 조합하면 응답 판정만 보는 검증기는 통과시켜 버립니다. 반대로 count가 1이어도 INVALID라면 가입 계약을 만족하지 않습니다. 정상 fixture의 필드 하나만 바꾸면 어느 단언이 필요한지 분리해 볼 수 있습니다. 실제 제품을 수정하지 않고 합성 변형을 입력해 검증기의 민감도를 확인합니다.
변형 검사가 실패하면 우선 기대값과 실제 값을 읽고 원본 계약을 확인합니다. 코드가 실패했다는 이유만으로 기대값을 완화하지 않습니다. 계약이 정말 변경된 경우라면 요구사항·테스트 표·기대값을 함께 검토한 뒤 변경 근거를 기록합니다. 임시로 문자열 숫자를 허용하면 다음 모듈 API 테스트도 같은 결함을 놓칠 수 있습니다. 검증기의 편의 수정은 제품 계약 변화와 같은 책임을 가집니다.
리뷰 가능한 작은 함수로 마칩니다
실습에서는 관찰 객체가 형태 계약을 지키는지 확인한 뒤 독립 expected와 비교합니다. 정상 가입, 정상 거절, 판정 불일치, 저장 수 불일치, 타입 오류를 각각 실행합니다. 실제 오류를 알려 주는 문자열에 회원 이메일이나 비밀번호를 넣지 않습니다. testId와 실패 필드만으로 사례를 찾을 수 있게 하여 앞 모듈의 증거 정리 원칙도 유지합니다.
제출 시에는 expected.result와 expected.count를 정한 요구사항을 한 문장으로 말합니다. TC-05의 상한 계약과 TC-26의 공백 거절 계약이 각각 어떤 실패를 잡는지 설명합니다. 모든 검사가 PASS인 자료만 보여 주지 않고 의도적으로 틀린 응답을 넣어 FAIL도 보여 줍니다. 이렇게 준비한 함수는 다음 레슨에서 비동기 응답을 받아도 같은 기준으로 판정하는 부품이 됩니다.
따라하기
합의와 관찰 분리
TC-05는 기대 성공을 고정합니다. 관찰 자료의 matches가 true여도 실제 판정과 독립 기대값을 비교합니다.
const expected={result:'VALID',count:1};
const actual={result:'INVALID',count:0,matches:true};
console.log('TC-05',actual.result===expected.result?'PASS':'FAIL result');실행 결과
TC-05 FAIL result
독립 객체 비교
속성이 같은 두 객체도 참조 비교는 false입니다. 구조 내용은 strict 모듈의 deepEqual로 확인합니다.
const a={count:1}; const b={count:1};
console.log(a===b);
const assert=require('node:assert/strict');
assert.deepEqual(a,b); console.log('contents PASS');실행 결과
false contents PASS
단언 오류에서 값 읽기
의도한 실패를 잡아 오류 코드와 실제·기대 값만 출력합니다. 인자 순서를 확인합니다.
const assert=require('node:assert/strict');
try { assert.equal(0,1,'TC-05 count'); }
catch(e) { console.log(e.code, 'actual='+e.actual, 'expected='+e.expected); }실행 결과
ERR_ASSERTION actual=0 expected=1
거절을 기대하는 검사
TC-26 결함 입력이 예외를 던지면 검출 테스트는 통과합니다. 출력의 detector PASS는 제품 성공을 뜻하지 않습니다.
const assert=require('node:assert/strict');
function check(x){assert.equal(x.result,'BLANK_NICKNAME');}
assert.throws(()=>check({result:'VALID'}),{code:'ERR_ASSERTION'});
console.log('TC-26 detector PASS');실행 결과
TC-26 detector PASS
확인 문제
실습
입력은 id 문자열과 expected 객체(result 문자열·count 비음수 안전 정수), actual 값입니다. actual의 본문 형태·result 타입·count 타입을 먼저 검사합니다. 그 뒤 result 값, count 값 순서로 독립 expected와 엄격 비교합니다. 첫 실패를 id FAIL body 또는 result:type·count:type·result:value·count:value로 출력합니다. 모두 같으면 id PASS입니다. actual.expected·matches는 무시합니다. expected와 id는 유효한 입력으로 제공됩니다.
모범 답안
const fs=require('fs');
const value=JSON.parse(fs.readFileSync(0,'utf8'));
function compare(x){
const a=x.actual,e=x.expected;
if(a===null||typeof a!=='object'||Array.isArray(a))return x.id+' FAIL body';
if(typeof a.result!=='string')return x.id+' FAIL result:type';
if(!Number.isSafeInteger(a.count)||a.count<0)return x.id+' FAIL count:type';
if(a.result!==e.result)return x.id+' FAIL result:value';
if(a.count!==e.count)return x.id+' FAIL count:value';
return x.id+' PASS';
}
console.log(compare(value));
더 읽기
면접 질문
- 요구사항에 정상 동작만 적혀 있을 때 대응 방법을 설명해 주시면 됩니다.