Logo

Jest로 파라미터화 테스트하기: test.each(), describe.each()

테스트를 작성하다보면 다양한 테스트 데이터에 대해서 동일한 테스트 코드를 돌리고 싶을 때가 있죠? 이러한 테스팅 기법을 보통 파라미터화(parameterized) 테스팅이라고 하는데요.

이번 글에서는 Jest에서 제공하는 test.each()describe.each() 함수를 사용하여 파라미터화 테스트를 하는 방법에 대해서 배워보겠습니다.

파라미터화(parameterized) 테스트

간단한 실습을 위해 2개의 문자열의 인자로 받아 애너그램(anagram) 여부를 반환해주는 함수를 작성해볼까요?

index.js
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);
}

이제 위 함수에 대한 테스트를 작성하고 실행해보겠습니다.

1.test.js
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);
});
Terminal
$ 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() 함수의 인자로 넘기면, 배열을 루프 돌면서 각 테스트 데이터를 대상으로 테스트 함수를 호출해줍니다. 뿐만 아니라, 테스트 이름에도 테스트 데이터 값을 삽입해주기 때문에 여러 테스트 간에 구분을 용이하게 하는데 활용할 수 있습니다.

2.test.js
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);
});
Terminal
$ 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() 함수를 사용하실 수 있습니다.

2-1.test.js
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() 함수는 여러 테스트 함수를 여러 테스트 데이터를 대상으로 실행해야할 때 사용합니다.

예를 들어, 위에서 작성한 애너그램 함수가 추가적인 옵션을 받을 수 있도록 코드를 살짝 변경 해보겠습니다.

index.js
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() 함수를 이용해서 작성해보겠습니다.

3.test.js
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);
  });
});
Terminal
$ 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 태그를 통해서 쉽게 만나보세요!