Devin.KR

로컬 테스트 실행과 회귀 증명

100분 안팎

학습 목표

node:test 결과와 종료 코드를 읽습니다.

개념

한 번의 초록 결과보다 잡아내는 실패를 봅니다

검증 함수가 true만 반환하면 정상 응답 테스트는 모두 통과할 수 있습니다. 하지만 null이나 결함 응답도 통과시키므로 QA 도구로는 역할을 하지 못합니다. 이번 레슨에서는 node:test로 정상과 잘못된 응답을 같이 실행하고, 실패해야 할 입력을 거절한다는 사실까지 확인합니다. 테스트 수와 종료 코드를 함께 읽어 자동 회귀 검증이 실제로 수행되었는지 판단합니다.

qa-test-runner의 checks/response.cjs에는 두 미완성 함수가 있습니다. validateResponse는 타입·필수 값·기대값을 검사하고 validateAsync는 loader 완료와 오류 전달을 담당합니다. tests 파일에는 의도를 검토할 수 있는 사례가 준비되어 있습니다. 검사기를 고쳐 테스트를 통과하는 것이 과제이며 테스트 삭제나 기대값 완화는 해결 방법이 아닙니다. 정상 검증과 결함 검출을 나란히 확인합니다.

테스트 한 개를 세 부분으로 읽습니다

require('node:test')에서 test를 가져오고 require('node:assert/strict')에서 assert를 가져옵니다. test의 첫 인자는 조건과 기대 결과를 담은 이름이고 두 번째 인자는 실제 작업 함수입니다. 이름 앞 CODE-17 같은 ID는 실패를 빨리 찾게 합니다. 준비에서 응답과 독립 기대값을 만들고, 실행에서 검사 함수를 부르며, 검증에서 정상 반환 또는 예외를 확인합니다.

정상 사례는 assert.equal(validateResponse(...), true)로 확인합니다. 잘못된 본문은 assert.throws(() => validateResponse(...), {code:'ERR_ASSERTION'})로 확인합니다. 이 테스트의 통과는 오류가 없는 입력이라는 뜻이 아니라 의도한 단언 실패를 잡았다는 뜻입니다. throws 밖에서 AssertionError가 나면 테스트가 실패하고, throws 안에서 예상 오류가 나오면 테스트는 통과합니다.

비동기 테스트 함수에는 async를 붙이고 await assert.rejects로 거절을 확인합니다. loader의 오류는 같은 객체인지 비교하여 원본 오류가 성공으로 바뀌거나 새 오류로 대체되지 않는지 확인합니다. 테스트 함수가 반환한 Promise가 끝나야 러너도 해당 테스트를 완료합니다. await 누락과 원본 예외 삼킴은 서로 다른 문제이며 사례 이름도 구별합니다.

형태·의미·시간을 따로 검사합니다

형태 검사에는 null·배열·문자열 본문, 누락 result, 문자열 count, 공백 message와 requestId가 있습니다. 회원 수의 숫자 경계에는 0, 음수, 소수, NaN, Infinity, 안전 정수 범위 밖 값도 넣습니다. JSON에서 NaN이나 Infinity를 표현할 수 없지만 JavaScript 함수 인자로는 전달할 수 있어 로컬 검사기로 확인합니다. 브라우저 입력과 함수 호출의 표현 범위를 구별합니다.

의미 검사에서는 정상 result를 INVALID로 바꾸거나 정상 count를 0으로 바꾼 한 필드 변형을 씁니다. 타입은 그대로여서 형태 검사만 있는 코드라면 놓치는 사례입니다. 앞 레슨의 비교 함수를 재사용하되 각 기대값은 인수 기준에서 고정합니다. 두 변형 중 하나만 놓치면 그 필드에 필요한 단언이 빠졌다는 단서가 됩니다. 테스트가 어느 책임을 확인하는지 분리하면 수정 범위도 작아집니다.

시간 검사에서는 loader의 Promise를 보류한 채 완료 표시를 관찰합니다. resolve 전에 true를 반환하는 변형은 조기 완료로 실패해야 합니다. 나중에 잘못된 본문으로 완료하는 loader도 assert.rejects로 기다립니다. 네트워크 라이브러리나 타이머를 설치하지 않고 완료 경계 자체를 검증합니다. 실제 API의 시간 제한과 취소는 다음 단계에서 별도 요구로 다룹니다.

실패 출력을 조사 순서로 읽습니다

starter에서 bash check.sh를 실행하면 정상 사례 일부는 통과하고 여러 거절 검사는 실패합니다. Missing expected exception은 예상한 예외가 발생하지 않았다는 뜻입니다. 현재 코드는 타입과 기대값을 확인하지 않고 반환하므로 오류가 없는 것이 오히려 문제입니다. 함수가 false를 반환하는 것과 AssertionError를 던지는 것은 계약이 다릅니다. 제공 테스트는 예외로 실패를 전달하는 함수를 요구합니다.

Expected values to be strictly equal 메시지에서는 actual과 expected를 먼저 비교합니다. count 값이 0과 1로 갈렸다면 test ID를 통해 가입 성공의 저장 증가 인수 기준을 읽습니다. 스택에서는 Node 내부 프레임보다 첫 checks/ 파일과 줄을 찾습니다. MODULE_NOT_FOUND는 경로·파일명·실행 위치를, SyntaxError는 문법을 먼저 확인합니다. 실패 종류를 제품 결함으로 단정하기 전에 실행된 대상과 계약을 확인합니다.

test 출력의 tests·pass·fail뿐 아니라 cancelled·skipped·todo도 확인합니다. 전체 명령의 종료 코드 0만으로 목표를 모두 실행했다는 뜻은 아닙니다. 빈 실행이나 전부 skip도 종료 상태가 기대와 다를 수 있어 예정된 개수와 이름을 대조합니다. 이번 완성본은 레슨 실습 22개, 미션 31개를 모두 실행하며 실패·취소·건너뜀·todo가 0이어야 합니다. 소요 시간은 환경마다 달라 고정하지 않습니다.

작게 수정하고 같은 대상을 재실행합니다

먼저 validateResponse에서 null과 배열을 제외하고 각 필드 타입을 확인합니다. 공백 문자열과 비음수 안전 정수 조건을 덧붙인 다음 result와 count를 expected와 비교합니다. 각 assert에는 실패 필드를 설명하는 메시지를 씁니다. validateAsync에서는 await load() 뒤에 같은 함수를 호출하고 원본 오류가 바깥으로 전달되게 합니다. 오류를 성공으로 바꾸는 catch를 제거해도 호출자에게 전달할 수 있습니다.

수정 후 같은 bash check.sh를 실행합니다. 검사를 줄여 초록으로 만드는 대신 처음 실패한 테스트 이름이 이제 통과하는지 읽습니다. 특정 사례만 빠르게 확인하려면 node --test --test-name-pattern='CODE-17' checks/response.test.cjs로 실행할 수 있습니다. 선택 실행 뒤에는 전체 명령도 실행합니다. 선택되지 않은 테스트가 건너뛰어졌다면 그 결과를 전체 검증으로 보고하지 않습니다.

이전 결함을 회귀 증거로 연결합니다

미션 starter는 앞 모듈 solution을 이어받고 checks를 추가합니다. 이전 check.sh와 README는 previous-check.sh와 previous-README.md로 보존하며 나머지 원본은 같은 위치에 있습니다. m04-inherited-sha256.json과 check-inherited.py는 원본 변경을 검사합니다. app의 Maven wrapper와 Java 코드도 그대로 있습니다. 이번 check.sh는 Java를 새로 실행하지 않고 보존된 응답 증거를 JavaScript 검증기에 넣습니다.

checks/traceability.json은 TC-05와 TC-26을 요구사항·인수 기준·결함 보고서에 연결합니다. evidence.test.cjs는 fixed가 기준을 만족하고 buggy가 같은 기준으로 거절되는지 확인합니다. buggy의 matches를 true로 조작해도 통과하면 안 됩니다. 관찰 자료의 판정 요약이 아니라 actual과 afterCount를 독립 expected에 대조합니다. 결함 fixture를 검출한 테스트의 통과는 제품 buggy 버전이 정상이라는 뜻이 아닙니다.

미션 결과에는 무엇을 자동화했고 무엇을 확인하지 않았는지를 함께 설명합니다. 기존 HTTP 증거를 검증기에 재생했지만 새 네트워크 요청과 브라우저 화면은 실행하지 않았습니다. 이전 15개 blocked를 통과로 바꾸지 않습니다. 로그인·프로필·권한은 다음 모듈 이후의 범위입니다. 특정 함수의 회귀 검사가 전체 서비스의 품질 결론으로 확대되지 않게 합니다.

완료 기준은 정상 fixture 수용, 결함 fixture 거절, 타입·필수값·비동기 거절 검출, 원본 보존입니다. 동료에게 CODE ID와 TC ID의 역할 차이를 설명하고 실패 메시지에서 인수 기준을 찾게 합니다. 실행 명령과 테스트 개수, 실패 0, 자동화 제외 범위를 기록합니다. 검사기가 완성되면 다음 모듈에서 실제 HTTP 응답을 같은 함수에 넣어 계약과 저장 효과를 이어서 확인할 수 있습니다.

따라하기

동기 실패 단언

문자열 회원 수를 거절하는 검사기를 확인합니다. 예외가 기대대로 발생하면 검출 테스트는 통과합니다.

const assert=require('node:assert/strict');
function validate(x){assert.equal(typeof x.count,'number');}
assert.throws(()=>validate({count:'1'}),{code:'ERR_ASSERTION'});
console.log('CODE-type detector PASS');

실행 결과

CODE-type detector PASS

비동기 실패 단언

거절 검증 자체의 Promise까지 await합니다. 동기 throws와 비동기 rejects의 차이를 설명합니다.

const assert=require('node:assert/strict');
async function main(){
 await assert.rejects(async()=>{throw new Error('loader rejected');},/loader rejected/);
 console.log('CODE-rejection detector PASS');
}
main().catch(e=>{console.error(e);process.exitCode=1;});

실행 결과

CODE-rejection detector PASS

starter 실행과 실패 위치 찾기

qa-test-runner starter의 루트에서 실행합니다. CODE ID, Missing expected exception, actual·expected와 첫 checks 경로를 읽습니다. starter는 22개 중 거절 검사의 일부가 실패해야 합니다. 실행 시간과 출력 순서는 고정하지 않습니다.

node --test checks/response.test.cjs

완성한 검사기 재실행

checks/response.cjs의 두 함수를 완성한 뒤 같은 루트에서 실행합니다. 전체 22개가 통과하고 fail·cancelled·skipped·todo가 0인지 확인합니다. 미션에서는 같은 명령이 해시 검사와 31개를 확인합니다.

bash check.sh

확인 문제

실습

checks/response.cjs의 validateResponse와 validateAsync만 완성합니다. 타입·공백 필수 값·안전 정수·독립 result/count 비교에서 AssertionError를 던지고 정상만 true를 반환합니다. 비동기는 loader 완료 뒤 검사하며 원본 예외를 전달합니다. bash check.sh로 22개를 확인합니다. 제공 테스트와 기대값은 수정하지 않습니다. 미션은 이전 원본을 이어받은 qa-validation-code-mission에서 31개와 해시 보존을 확인합니다.

시작 코드·테스트 내려받기

실행 명령

bash check.sh

기대 결과

node:test 22개 실행·통과, 실패·취소·skip·todo 0, 종료 코드 0

모범 답안모범 답안 내려받기

더 읽기

면접 질문

  • API 응답 코드가 성공이어도 테스트가 실패할 수 있는 상황을 설명해 주시면 됩니다.