export function add(a,b) {
const sum = a + b;
if (sum > 100) {
return 100;
}
return sum;
}
//BAD test
test("50 +50은 100", () => {
expect(add(50,50)).toBe(100);
)
test("70 + 80은 100", () => {
expect(add(70,80)).toBe(100);
})
테스트는 통과했지만 70 + 80이 100이 된다는 것은 이해할 수 없다.
따라서 테스트 명을 함수 기능에 부합하는 더 적절한 이름을 변경해야 한다.
test("반환값은 첫 번째 매개변수와 두 번째 매개변수를 더한 값이다", () => {
expect(add(50,50)).toBe(100);
)
test("반환값의 상한은 '100'이다.", () => {
expect(add(70,80)).toBe(100);
})
test 함수를 작성할 때 테스트 코드가 어떤 의도로 작성됐으며, 어떤 작업이 포함됐는지 테스트명으로 명확하게 표현해야다 한다.
에지 케이스와 예외 처리
모듈을 사용할 때 실수 등의 이유로 예상하지 못한 입력값을 보낼 때가 있다.
만약 모듈에 예외 처리를 했다면 예상하지 못한 입력값을 받았을 때 실행 중인 디버거로 문제를 빨리 발견할 수 있다.
타입스크립트로 입력값 제약 설정
타입스크립트를 사용한다면 함수의 매개변수에 타입을 붙여 다른 타입의 값이 할당되면 실행하기 전에 오류를 발생시킨다.
export function add(a:number, b: number) {
const sum = a + b;
if(sum > 100){
return 100;
}
return sum
}
하지만 정적 타입을 붙이는 것만으로는 부족할 때가 있다.
예를 들어 특정 범위로 입력값을 제한하고 싶을 때는 런타임에 예외를 발생시키는 처리를 추가해야한다.
예외 발생시키기
ex: add 함수에 매개변수 a,b는 0에서 100까지 숫자만 받을 수 있다는 조건을 추가해보자.
타입만으로는 커버할 수 없다.
export function add(a:number, b: number) {
if (a < 0 || a > 100) {
throw new Error("0~100 사이의 값을 입력해주세요");
}
if (b < 0 || b > 100) {
throw new Error("0~100 사이의 값을 입력해주세요");
}
const sum = a + b;
if(sum > 100){
return 100;
}
return sum
}
// 잘못된 예외 검증 코드 작성법
expect(add(-10, 110)).toThrow();
// 올바른 작성법 - 화살표 함수를 사용하면 함수에서 예외가 발생하는지 검증할 수 있다.
expect(() => add(-10, 110)).toThrow();
오류 메시지를 활용한 세부 사항 검증
예외 처리용 매처인 toThrow에 인수를 할당하면 예외에 대해 더욱 상세한 내용을 검증할 수 있다.
Error 인스턴스를 생성하면서 메시지를 인수로 할당
test("인수가 '0~100'의 범위 밖이면 예외가 발생한다", () => {
expect(() => add(110, -10)).toThrow('0~100 사이의 값을 입력해주세요");
})
의도적으로 예외를 발생시키기도 하지만 의도치 않은 버그가 생겨서 발생할 때도 있다.
의도했는지 아닌지 구분하기 위해 '의도한 대로 예외가 발생하고 있는가'라는 관점으로 접근하자.
instanceof 연산자를 활용한 세부 사항 검증
Error 클래스를 더욱 구체적인 상황에 맞춰 작성해보자.
설계의 폭을 넓힐 수 있다.
Error 클래스를 상속받은 두 개의 클래스 HttpError, RangeError 로 생성된 인스턴스는 instanceof 연산자를 사용해서 다른 인스턴스와 구분할 수 있다.
export class HttpError extends Error {}
export class RangeError extends Error {}
if (err instanceof HttpError) {
// 발생한 오류가 HttpError인 경우
}
if (err instanceof RangeError) {
// 발생한 오류가 RangeError인 경우
}
상속받은 클래스들을 활용해 입력값을 체크하는 함수를 작성해보자.
function checkRange(value: number) {
if (value < 0 || value > 100) {
throw new RangeError('0~100 사이의 값을 입력해주세요');
}
}
기존 add 함수에서 a,b를 검증해서 예외를 발생시키는 부분을 checkRange 함수 한곳에서 처리할 수 있어서 더 좋은 코드가 된다.
export function add(a: number, b: number) {
checkRange(a);
checkRange(b);
const sum = a + b;
if(sum > 100){
return 100;
}
return sum
}
toThrow 매처의 인수에는 메시지뿐만아니라 클래스도 할당이 가능하다.
예외가 특정 클래스의 인스턴스인지 검증할 수 있다.
// 발생한 예외가 RangeError이므로 실패
expect(() => add(110,-10)).toThrow(HttpError);
// 발생한 예외가 RangeError이므로 성공
expect(() => add(110, -10)).toThrow(RangeError);
// 발생한 예외가 Error를 상속받은 클래스이므로 성공 - Error를 상속받은 클래스이므로 테스트 성공함 (주의)
expect(() => add(110, -10)).toThrow(Error);
외부 API에서 데이터를 취득하거나 파일을 읽는 등 온갖 작업에서 비동기 처리가 필요하다.
export function wait(duration: number) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(duration);
}, duration)
})
}
Promise를 반환하는 방법
Promise 를 반환하면서 then 에 전달할 함수에 단언문을 작성하는 방법
wait 함수를 실행하면 Promise 인스턴스가 생성된다.
해당 인스턴스를 테스트 함수의 반환값으로 리턴하면 Promise 가 처리 중인 작업이 완료될 떄까지 테스트 판정을 유예한다.
test("지정 시간을 기다린 뒤 경과 시간과 함께 resolve 된다", () => {
return wait(50).then((duration) => {
expect(duration).toBe(50)
})
})
두 번째 방법은 resolve 매처를 사용하는 단언문을 리턴하는 것
wait 함수가 resolve 됐을 때의 값을 검증하고 싶다면 첫번째 방법보다 간편하다.
test("지정 시간을 기다린 뒤 경과 시간과 함께 resolve된다", () => {
return expect(wait(50)).resolves.toBe(50);
})
async/await을 활용한 방법
테스트 함수를 async 함수로 만들고 나서 함수 내에서 Promise가 완료될 때까지 기다리는 방법
resolves 매처를 사용하는 단언문도 await 로 대기시킬 수 있다.
test("지정 시간을 기다린 뒤 경과 시간과 함께 resolve 된다", async () => {
await expect(wait(50)).resolves.toBe(50);
})
네 번째 방법은 검증값인 Promise가 완료되는 것을 기다린 뒤 단언문을 실행하는 것
가장 간단한 방법
test("지정 시간을 기다린 뒤 경과 시간과 함께 resolve 된다", async () => {
expect(await wait(50)).toBe(50);
})
async/await 함수를 사용하면 비동기 처리가 포함된 단언문이 여럿일 때 한 개의 테스트 함수 내에서 정리할 수 있는 장점이있음
Reject 검증 테스트
반드시 reject 되는 코드의 함수를 사용해 reject 된 경우를 검증하는 테스트를 작성
export function timeout(duration: number) {
return new Promise((_, reject) => {
setTimeout(() => {
reject(duration);
}, duration)
})
}
// Promise를 리턴하는 방법 - catch 메서드에 전달할 함수에 단언문을 작성
test("지정 시간을 기다린뒤 경과 시간과 함께 reject 된다.", () => {
return timeout(50).catch((duration) => {
expect(duration).toBe(50);
})
})
// rejects 매처를 사용하는 단언문 활용 - 단언문을 리턴하거나 async/await을 사용
test("지정 시간을 기다린뒤 경과 시간과 함께 reject 된다.", () => {
return expect(timeout(50)).reject.toBe(50);
})
test("지정 시간을 기다린뒤 경과 시간과 함께 reject 된다.", async () => {
await expect(timeout(50)).reject.toBe(50);
})
// try/catch 문을 사용하는 방법 Unhandled Rejection을 try 블록에서 발생시키고,
// 발생한 오류를 catch 블록에서 받아 단언문으로 검증
test("지정 시간을 기다린뒤 경과 시간과 함께 reject 된다.", async () => {
expect.assertions(1); // 단언문이 한 번 실행되는 것을 기대하는 테스트가 된다.
try {
await timeout(50);
} catch (err) {
expect(err).toBe(50);
}
})
테스트 결과가 기댓값과 일치하는지 확인
실수로 작성된 코드가 있을 때 실행하고 싶은 단언문에 도달하지 못한 채로 성공하며 종료된다.
test("지정 시간을 기다린뒤 경과 시간과 함께 reject 된다.", async () => {
try {
await wait(50); // timeout 함수를 사용할 생각이었지만 실수로 wait 함수를 사용함
// 오류가 발생하지 않으므로 여기서 종료되면서 테스트는 성공
} catch (err) {
// 단언문은 실행되지 않음
expect(err).toBe(50);
}
})
이와 같은 실수를 하지 않으려면 테스트 함수 첫 줄에 expect.assertions를 호출해야 한다.
이 메서드는 실행되어야 하는 단언문의 횟수를 인수로 받아 기대한 횟수만큼 단언문이 호출됐는지를 검증한다.
test("지정 시간을 기다린뒤 경과 시간과 함께 reject 된다.", async () => {
expect.assertions(1); // 단언문이 한 번 실행되는 것을 기대하는 테스트가 된다.
try {
await wait(50);
// 단언문이 한 번도 실행되지 않은채로 종료되므로 테스트는 실패
} catch (err) {
expect(err).toBe(50);
}
})
비동기 처리 테스트를 할 때는 첫 번째 줄에 expect.assertions 을 추가하면 사소한 실수를 줄일 수 있다.
비동기 처리 테스트는 다양한 방식으로 작성할 수 있으니 자유롭게 선택하면 되지만 .resolves, .rejects 매처를 사용할 떄는 주의해야 한다.
테스트가 성공한것이 아니라 (*주의)단언문이 한 번도 평가되지 않고 종료될 때에도 테스트는 성공한다.
test("return하고 있지 않으므로 Promise가 완료되기 전에 테스트가 종료된다.", () => {
// 실패할 것을 기대하고 작성한 단언문
expect(wait(2000)).resolves.toBe(3000);
// 올바르게 고치려면 다음 주석처럼 단언문을 return 해야 한다.
// return expect(wait(2000)).resolves.toBe(3000);
})
비동기 처리를 테스트할 때 테스트 함수가 동기 함수라면 반드시 단언문을 return 해야 한다.
테스트에 따라서는 단언문을 여러 번 작성해야 할 때가 있는데 단언문을 여러번 작성하다 보면 return 하는 것을 잊기 쉽다.
이같은 실수를 하지 않으려면 비동기 처리가 포함된 테스트를 할때 다음 과 같은 규칙을 갖고 접근해야 한다.
비동기 처리가 포함된 부분을 테스트할 때 테스트 함수를 async 함수로 만든다.
.resolves, .rejects 가 포함된 단언문은 await 한다.
try-catch 문의 예외 발생을 검증할 때는 expect.assertions 를 사용한다.