Jest로 파라미터화 테스트하기: test.each(), describe.each()
테스트를 작성하다보면 다양한 테스트 데이터에 대해서 동일한 테스트 코드를 돌리고 싶을 때가 있죠? 이러한 테스팅 기법을 보통 파라미터화(parameterized) 테스팅이라고 하는데요.
이번 글에서는 Jest에서 제공하는 test.each()
와 describe.each()
함수를 사용하여 파라미터화 테스트를 하는 방법에 대해서 배워보겠습니다.
파라미터화(parameterized) 테스트
간단한 실습을 위해 2개의 문자열의 인자로 받아 애너그램(anagram) 여부를 반환해주는 함수를 작성해볼까요?
function areAnagrams(first, second) {
const counter = {};
for (const ch of first) {
counter[ch] = (counter[ch] || 0) + 1;
}
for (const ch of second) {
counter[ch] = (counter[ch] || 0) - 1;
}
return Object.values(counter).every((cnt) => cnt == 0);
}
이제 위 함수에 대한 테스트를 작성하고 실행해보겠습니다.
import { areAnagrams } from "./";
test("car and bike are not anagrams", () => {
expect(areAnagrams("car", "bike")).toBe(false);
});
test("car and arc are anagrams", () => {
expect(areAnagrams("car", "arc")).toBe(true);
});
test("cat and dog are not anagrams", () => {
expect(areAnagrams("cat", "dog")).toBe(false);
});
test("cat and act are anagrams", () => {
expect(areAnagrams("cat", "act")).toBe(true);
});
$ jest 1.test.js
PASS ./1.test.js (3.943 s)
✓ car and bike are not anagrams (19 ms)
✓ car and arc are anagrams (2 ms)
✓ cat and dog are not anagrams (1 ms)
✓ cat and act are anagrams (4 ms)
Test Suites: 1 passed, 1 total
Tests: 4 passed, 4 total
Snapshots: 0 total
Time: 4.535 s
Ran all test suites matching /1.test.js/i.
위 4개의 테스트 함수를 살펴보면, 테스트 데이터만 빼만 동일한 코드라는 것을 알 수 있습니다.
이런 식으로 테스트를 작성하면 함수의 이름이나 매개변수가 바뀌었을 때 어러 곳을 반복해서 수정해줘야 해서 이상적이지 않은데요. 어떻게 하면 반복되는 테스트 코드를 제거하면서, 다양한 테스트 데이터에 대한 테스트를 작성할 수 있을까요?
파라미터화(parameterized) 테스트는 이러한 상황에서 유용하게 사용할 수 있는 테스팅 기법입니다.
Jest에서는 파라미터화 테스트를 지원하기 위해서 test.each()
와 describe.each()
함수를 제공하고 있습니다.
test.each() 함수
먼저 test.each()
함수를 사용해서 파라미터화 테스트를 작성해보겠습니다.
테스트 데이터를 2차원 배열에 담아서 test.each()
함수의 인자로 넘기면, 배열을 루프 돌면서 각 테스트 데이터를 대상으로 테스트 함수를 호출해줍니다.
뿐만 아니라, 테스트 이름에도 테스트 데이터 값을 삽입해주기 때문에 여러 테스트 간에 구분을 용이하게 하는데 활용할 수 있습니다.
import { areAnagrams } from "./";
test.each([
["cat", "bike", false],
["car", "arc", true],
["cat", "dog", false],
["cat", "act", true],
])("areAnagrams(%s, %s) returns %s", (first, second, expected) => {
expect(areAnagrams(first, second)).toBe(expected);
});
$ jest "2.test.js"
PASS ./2.test.js (4.119 s)
✓ areAnagrams(cat, bike) returns false (8 ms)
✓ areAnagrams(car, arc) returns true (5 ms)
✓ areAnagrams(cat, dog) returns false (2 ms)
✓ areAnagrams(cat, act) returns true (2 ms)
Test Suites: 1 passed, 1 total
Tests: 4 passed, 4 total
Snapshots: 0 total
Time: 5.168 s
참고로 test
의 별칭(alias)인 it
을 통해서도 동일한 방식으로 each()
함수를 사용하실 수 있습니다.
import { areAnagrams } from "./";
describe("areAnagrams()", () => {
it.each([
["cat", "bike", false],
["car", "arc", true],
["cat", "dog", false],
["cat", "act", true],
])("areAnagrams(%s, %s) returns %s", (first, second, expected) => {
expect(areAnagrams(first, second)).toBe(expected);
});
});
describe.each() 함수
좀 더 복잡한 함수에 대한 파라미터화 테스트를 작성할 때는 describe.each()
함수를 이용하면 됩니다.
describe.each()
함수는 여러 테스트 함수를 여러 테스트 데이터를 대상으로 실행해야 할 때 사용합니다.
예를 들어, 위에서 작성한 애너그램 함수가 추가적인 옵션을 받을 수 있도록 코드를 살짝 변경 해보겠습니다.
function areAnagrams(
first,
second,
options = { ignoreCase: false, ignoreSpaces: false }
) {
if (options.ignoreCase) {
first = first.toLowerCase();
second = second.toLowerCase();
}
if (options.ignoreSpaces) {
first = first.replace(/ /g, "");
second = second.replace(/ /g, "");
}
const counter = {};
for (const ch of first) {
counter[ch] = (counter[ch] || 0) + 1;
}
for (const ch of second) {
counter[ch] = (counter[ch] || 0) - 1;
}
return Object.values(counter).every((cnt) => cnt == 0);
}
이제 이 함수는 동일한 인자가 주어지더라도 어떤 옵션을 사용했는지에 따라 다른 결과를 반환할 수 있게 되었습니다.
따라서 옵션 여부에 따라 함수가 제대로 동작하는지 describe.each()
함수를 이용해서 작성해보겠습니다.
import { areAnagrams } from "./";
describe.each([
["Cat", "Act"],
["Save", "Vase"],
["Elbow", "Below"],
])("areAnagrams(%s, %s)", (first, second) => {
it("return true with ignoreCase option", () => {
expect(areAnagrams(first, second, { ignoreCase: true })).toBe(true);
});
it("return false without ignoreCase option", () => {
expect(areAnagrams(first, second)).toBe(false);
});
});
describe.each([
["dormitory", "dirty room"],
["conversation", "voices rant on"],
])("areAnagrams(%s, %s)", (first, second) => {
it("return true with ignoreSpaces option", () => {
expect(areAnagrams(first, second, { ignoreSpaces: true })).toBe(true);
});
it("return false without ignoreSpaces option", () => {
expect(areAnagrams(first, second)).toBe(false);
});
});
$ jest "3.test.js"
PASS ./3.test.js
areAnagrams(Cat, Act)
✓ return true with ignoreCase option (4 ms)
✓ return false without ignoreCase option (1 ms)
areAnagrams(Save, Vase)
✓ return true with ignoreCase option (1 ms)
✓ return false without ignoreCase option (1 ms)
areAnagrams(Elbow, Below)
✓ return true with ignoreCase option
✓ return false without ignoreCase option (2 ms)
areAnagrams(dormitory, dirty room)
✓ return true with ignoreSpaces option (1 ms)
✓ return false without ignoreSpaces option (1 ms)
areAnagrams(conversation, voices rant on)
✓ return true with ignoreSpaces option
✓ return false without ignoreSpaces option (1 ms)
Test Suites: 1 passed, 1 total
Tests: 10 passed, 10 total
Snapshots: 0 total
Time: 6.977 s, estimated 8 s
Ran all test suites matching /3.test.js/i.
전체 코드
본 포스팅에서 작성한 코드는 아래에서 확인하시고 바로 실행해보실 수 있습니다.
마치면서
이상으로 Jest의 test.each()
와 describe.each()
함수를 사용하여 여러 테스트 데이터를 대상으로 테스트 함수를 실행하는 방법에 대해서 살펴보았습니다.
파라미터화(parameterized) 테스트를 적지적소에 잘 활용하셔서 좀 더 깔끔하고 유지보수가 쉬운 테스트 코드를 작성하시길 바랍니다.
Jest에 연관된 포스팅은 Jest 태그를 통해서 쉽게 만나보세요!