제스트(JEST)
테스트 프레임워크인 제스트는 2가지 방법으로 테스트 파일을 찾습니다.
- __test__ 폴더 안의 모든 파일을 불러온다.
- 모든 폴더를 대상으로 *.spec.js 또는 .test.js로 끝나는 파일 재귀적으로 찾는다. (.ts 포함)
제스트에서는 test() 함수나 it() 함수를 통해 개별 테스트를 구현합니다. 이때, 폴더 구조의 역할과 유사한 describe() 함수를 사용하면 테스트를 그룹으로 묶을 수 있으며, 주로 it() 함수와 함께 사용됩니다. describe() 구문은 주로 동일한 시나리오에서 동일한 진입점에 대해 여러 결과를 검증할 때 사용합니다.
중첩된 코드를 표현하기 위해 beforeEach() 함수를 사용할 수 있지만, 너무 과도한 결합으로 유연성이 떨어지거나 테스트가 어떤 일을 하는지 이해하기 위해 ‘스크롤 피로감’이 생길 수도 있으므로 사용 전에 적절성에 대한 고민이 필요합니다.
테스트 작성 방법
USE 전략
USE 전략은 테스트 이름을 잘 짓는 방법입니다. 이름에 테스트의 대상, 상황, 결과에 대한 정보를 표현하면 어떤 테스트를 하는지 명확하게 파악할 수 있습니다.
- Unit - 테스트하려는 대상
- Scenario - 입력 값이나 상황에 대한 설명
- Expectation - 기댓값이나 결과에 대한 설명
AAA 패턴
AAA(Arrange-Act-Assert: 준비-실행-검증) 패턴은 테스트를 간단하고 일관된 방식으로 작성할 수 있도록 돕는 패턴입니다.
- 준비(Arrange) 단계 - 테스트하려는 시스템과 그 종속성을 원하는 상태로 설정
- 실행(Act) 단계 - 메서드를 호출하고 필요한 데이터를 전달하며, 그 결과 값을 확인
- 검증(Assert) 단계 - 결과를 검증
적용하기
describe() 함수를 활용하여 USE 전략과 AAA 패턴을 적용한 테스트 코드는 아래와 같습니다.
// USE 전략 - 대상
describe("verifyPassword", () => {
// USE 전략 - 시나리오
describe("with a failing rule", () => {
// USE 전략 - 기대값
it("returns error", () => {
// AAA 패턴 - 준비
const fakeRule = (input) => ({
passed: false,
reason: "fake reason",
});
// AAA 패턴 - 실행
const errors = verifyPassword("any value", [fakeRule]);
// AAA 패턴 - 검증
expect(errors[0]).toContain("fake reason");
});
});
});
결합도와 유연성 사이의 균형
문자열 비교
문자열에 대한 검증을 할 때, 완전히 일치하는지 비교하면 유지보수성이 떨어질 수 있습니다. 중요한 것은 테스트가 마침표나 쉼표까지 일치하는지를 검증하는 것이 아니라, 메시지가 담은 의미를 검증하도록 하는 것입니다. 따라서 문자열을 비교할 때는 정규식을 사용하는 toMatch(/string/) 패턴 또는 toContain() 메서드를 사용하는 것이 좋습니다.
expect(errors[0]).toContain("fake reason");
expect(errors[0]).toMatch("fake reason");
expect(errors[0]).toMatch(/fake reason/);
비밀번호 검증 로직 테스트 코드
describe() 구문을 활용해서 비밀번호 검증에 실패하는 케이스를 구현한 테스트 코드입니다.
describe("PasswordVerifier", () => {
describe("with a failing rule", () => {
it("has an error message based on the rule.reason", () => {
const verifier = new PasswordVerifier1();
const fakeRule = () => ({
passed: false,
reason: "fake reason",
});
verifier.addRule(fakeRule);
const errors = verifier.verify("any value");
expect(errors[0]).toContain("fake reason");
});
it("has exactly one error", () => {
const verifier = new PasswordVerifier1();
const fakeRule = () => ({
passed: false,
reason: "fake reason",
});
verifier.addRule(fakeRule);
const errors = verifier.verify("any value");
expect(errors.length).toBe(1);
});
});
});
beforeEach() 함수 사용
위의 테스트 코드를 보면 중복된 코드들이 많이 보입니다. 이때 beforeEach() 함수를 활용하면, 각 테스트가 실행되기 전에 실행될 코드를 모아둠으로써 중복을 제거할 수 있습니다. 그치만, beforeEach() 함수를 사용하는 경우 주의해야 할 것이 2가지 있습니다. 먼저, 초기화하는 부분이 없다면 잠정적인 사이드 이펙트를 야기할 수 있다는 점입니다. 또 하나는 병렬로 단위 테스트를 실행하는 제스트의 특성 상, 공유되는 변수가 있을 때 다른 테스트에 영향을 줄 수 있다는 점입니다.
그래서 테스트 코드에 한해서는 과도한 로직 공통화 작업이 되려 유연성을 떨어뜨릴 수도 있다는 것을 주의해야 합니다.
describe("PasswordVerifier", () => {
let verifier;
beforeEach(() => {
verifier = new PasswordVerifier1();
});
describe("with a failing rule", () => {
let fakeRule, errors;
beforeEach(() => {
fakeRule = () => ({
passed: false,
reason: "fake reason",
});
verifier.addRule(fakeRule);
errors = verifier.verify("any value");
});
it("has an error message based on the rule.reason", () => {
expect(errors[0]).toContain("fake reason");
});
it("has exactly one error", () => {
expect(errors.length).toBe(1);
});
});
describe("with a passing rule", () => {
let fakeRule, errors;
beforeEach(() => {
fakeRule = () => ({
passed: true,
reason: "",
});
verifier.addRule(fakeRule);
errors = verifier.verify("any reason");
});
it("has no errors", () => {
expect(errors.length).toBe(0);
});
});
});
팩토리 함수 사용
그런데 위처럼 beforeEach() 함수를 사용해도 시나리오가 추가될 경우 중복이 발생하며, 테스트 대상 등을 알아보기 위해 스크롤 피로감이 발생합니다. 이때, 팩토리 함수를 사용하면 가독성을 높이고 스크롤 피로감을 줄일 수 있습니다.
팩토리 함수는 객체나 특정 상태를 쉽게 생성하고, 여러 곳에서 동일한 로직을 재사용할 수 있도록 도와주는 간단한 헬퍼 함수입니다. 팩토리 함수를 적용하면 캡슐화를 했기 때문에 객체가 어떻게 생성되는지는 모르지만, 언제 생성되고 어떤 중요한 매개변수로 초기화되는지는 알 수 있기 때문에 스크롤 피로감이 줄어들게 됩니다.
const makeVerifier = () => new PasswordVerifier1();
const makeVerifierWithPassingRule = () => {
const verifier = makeVerifier();
const passingRule = () => ({ passed: true, reason: "" });
verifier.addRule(passingRule);
return verifier;
};
const makeVerifierWithFailingRule = (reason) => {
const verifier = makeVerifier();
const failingRule = () => ({ passed: false, reason: reason });
verifier.addRule(failingRule);
return verifier;
};
describe("PasswordVerifier", () => {
describe("with a passing rule", () => {
it("has no errors", () => {
const verifier = makeVerifierWithPassingRule();
const errors = verifier.verify("any input");
expect(errors.length).toBe(0);
});
});
describe("with a failing rule", () => {
it("has an error message based on the rule.reason", () => {
const verifier = makeVerifierWithFailingRule("fake reason");
const errors = verifier.verify("any input");
expect(errors[0]).toContain("fake reason");
});
it("has exactly one error", () => {
const verifier = makeVerifierWithFailingRule("fake reason");
const errors = verifier.verify("any input");
expect(errors.length).toBe(1);
});
});
});
test.each() - 다양한 입력값 테스트
입력값을 다양하게 넣으며 테스트하는 경우, test.each() 또는 it.each() 를 활용하여 테스트 중복을 제거할 수도 있습니다.
each() 안에 배열로 테스트할 대상을 나타내며, 설명 뒤에 이어지는 콜백에 매개변수로 넣어줍니다. 만약 매개변수를 2개 이상 사용할 경우, 배열 안에 배열로 값을 나타내며 매개변수도 그 숫자에 맞게 넣어준 뒤 사용합니다.
const oneUpperCaseRule = (input) => {
return {
passed: input.toLowerCase() !== input,
reason: "at least one upper case needed",
};
};
describe("one uppercase rule", () => {
describe("given no uppercase", () => {
it("fails", () => {
const result = oneUpperCaseRule("abc");
expect(result.passed).toEqual(false);
});
});
describe("given at least one uppercase", () => {
it.each(["Abc", "abC"])("passes", (input) => {
const result = oneUpperCaseRule(input);
expect(result.passed).toEqual(true);
});
});
test.each([
["Abc", true],
["aBc", true],
["abc", false],
])("given %s %s", (input, expected) => {
const result = oneUpperCaseRule(input);
expect(result.passed).toEqual(expected);
});
});
예정된 오류가 발생하는지 확인
에러를 테스트하는 경우, toThrowError() 메서드 사용합니다. 이때, expect() 메서드 안에는 변수가 아닌 콜백 함수로 에러가 발생하는 로직을 넣어야 합니다.
describe("with no rules", () => {
it("throw exception", () => {
const verifier = makeVerifier();
expect(() => verifier.verify("any input")).toThrowError(
/no rules configured/
);
});
});
참고자료
- 단위 테스트의 기술, 길벗
- 도서 코드 예제 깃헙
'Dev > 테스트 코드' 카테고리의 다른 글
비동기 코드 단위 테스트 (0) | 2025.03.16 |
---|---|
격리 프레임워크 (0) | 2025.03.07 |
모의 객체를 사용한 상호 작용 테스트 (0) | 2025.03.04 |
의존성 분리와 스텁 (0) | 2025.03.02 |
단위 테스트 기초 (0) | 2025.02.23 |