Logo

Vitest의 자주 쓰이는 매처 함수 총정리

Vitest는 테스트를 작성할 때 효과적인 검증이 가능하도록 Jest의 API와 호환이 되는 다양한 매처 함수를 제공하고 있습니다.

이번 포스팅는 실전 테스팅에서 많이 사용되는 매처 함수를 하나씩 살펴 보면서 함께 테스트를 작성하고 실행해보도록 하겠습니다.

차세대 테스팅 프레임워크로 각광받고 있는 Vitest에서 생소하시다면 먼저 소개 포스팅을 읽어 보시고 돌아오시기를 추천드립니다.

검증 (Assertion)

테스트라는 행위는 소프트웨어가 예상한데로 작동하는지를 확인하는 작업입니다. 이를 좀 어려운 말로 Assertion, 즉 검증 또는 단언이라고 하죠.

Vitest에서는 효과적인 검증을 위해서 expect() 함수와 여러 매처(matcher) 함수를 함께 사용합니다. 그래서 보통 테스트 내의 검증문 다음과 같은 형태를 띄며, 실제 값이 예상 값과 부합하는지를 확인하게 됩니다.

expect(실제 값).matchXxx(예상 값)

expect() 함수에 검증 대상을 넘겨서 호출하면, 다수의 매처 함수가 들어있는 객체가 반환됩니다. 우리는 상황에 맞게 적당한 매처 함수를 골라서 호출만 해주면 Vitest가 검증 작업을 수행해줍니다.

매처 함수는 검증이 실패할 경우 오류를 던지며, Vitest는 이 것을 잡아서 검증이 실패한 이유를 기발자가 이해하기 쉽게 콘솔에 출력해줍니다.

참고로 어떤 값이 예상 값과 부합하지 않는지를 검사하고 싶을 때는, 단순히 매처 함수 앞에 not.만 붙여주면 됩니다.

expect(실제 값).not.match(예상 값)

toBe()

가장 먼저 살펴볼 매처 함수는 원시(primitive) 자료형의 값을 검증할 때 사용하는 toBe()입니다. 이름만 보면 매우 많이 사용될 것 같지만 사실 활용 사례가 매우 제한적인 매처 함수입니다.

예를 들어, 아래와 같이 두 개의 숫자를 더해주는 add() 함수에 대한 테스트를 작성해보겠습니다.

function add(x: number, y: number) {
  return x + y;
}

add(1, 2)는 숫자 3을 반환해야하기 때문에 toBe() 함수를 사용하였습니다.

import { expect, test } from "vitest";

test("return the sum of the given two integers.", () => {
  expect(add(1, 2)).toBe(3);
});

테스트를 실행해보면 검증이 통과하는 것을 볼 수 있습니다.

$ npx vite

 RUN  src/add.test.ts x72

 ✓ src/add.test.ts (1)return the sum of the given two integers.

 Test Files  1 passed (1)
      Tests  1 passed (1)
   Start at  19:22:57
   Duration  8ms

이번에는 add() 함수의 인자로 두 개의 실수를 넘겨서 비슷하게 테스트해볼까요?

import { expect, test } from "vitest";

test("return the sum of the given two floats.", () => {
  expect(add(0.1, 0.2)).toBe(0.3);
});

테스트를 실행해보면 다음과 같이 오류를 내며 실패할 것입니다. 많은 분들이 아시는 것처럼 자바스크립트는 부동소수점 연산을 하기 때문에 0.1 더하기 0.2는 0.3이 아니기 때문입니다.

 ❯ src/add.test.ts (2)
   × return the sum of the given two floats.

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯

 FAIL  src/add.test.ts > return the sum of the given two floats.
AssertionError: expected 0.30000000000000004 to be 0.3 // Object.is equality

- Expected
+ Received

- 0.3
+ 0.30000000000000004eval src/add.test.ts:19:25
     17|
     18| test('return the sum of the given two floats.', () => {
     19|   expect(add(0.1, 0.2)).toBe(0.3);
       |                         ^
     20| });
     21|

이러한 문제를 해결하기 위해서 Vitest는 toBeCloseTo()라는 매처 함수도 제공하고 있습니다. toBe() 대신에 toBeCloseTo()를 사용하시면 테스트가 성공하는 것을 보실 수 있을 거에요.

test("return the sum of the given two floats.", () => {
  expect(add(0.1, 0.2)).toBeCloseTo(0.3);
});

toBe() 매처 함수는 객체를 대상으로 사용할 때는 각별한 주의가 필요한데요. 한번 빈 객체가 빈 객체와 같은지를 테스트해 보겠습니다.

test("an empty object is an empty object.", () => {
  expect({}).toBe({});
});

이 테스트가 실패해서 놀라시는 분들이 있을 것 같은데요. 😮 이 두 개의 객체는 형태는 동일하더라도 엄밀히 얘기해서 다른 레퍼런스, 즉 메모리 내에서 다른 주소이기 때문에 동일하다고 볼 수 없습니다.

 FAIL  src/dale.test.ts > an empty object is an empty object
AssertionError: expected {} to be {} // Object.is equality

If it should pass with deep equality, replace "toBe" with "toStrictEqual"

Expected: {}
Received: serializes to the same string


Compared values have no visual difference.

 ❯ eval src/dale.test.ts:23:14
     21|
     22| test('an empty object is an empty object', () => {
     23|   expect({}).toBe({});
       |              ^
     24| });
     25|

제가 이 것을 좀 더 명확히 보여드리기 위해서 변수에 빈 객체를 저장해놓고, 변수를 대상으로 toBe() 함수를 사용해볼께요. 이 테스트는 통과하는 것을 보실 수 있을 거에요. obj 변수는 동일한 객체의 레퍼런스를 저장하고 있기 때문입니다.

test("an empty object is an empty object.", () => {
  const obj = {};
  expect(obj).toBe(obj);
});

그럼 객체를 대상으로 검증을 할 때는 어떤 매처 함수를 사용해야할까요? 다음 섹션에서 알려드리겠습니다.

toEqual()

다음과 같이 사용자 번호를 넘기면 가짜 사용자 객체를 반환하는 함수를 테스트하려고 합니다.

function getUser(no: number) {
  return {
    no,
    email: `user${no}@test.com`,
  };
}

위에서 했던 방식으로 toBe() 매처 함수를 사용하면 테스트가 실패하는 것을 볼 수 있는데요.

import { expect, test } from "vitest";

test("return a user object.", () => {
  expect(getUser(1)).toBe({
    no: 1,
    email: `user1@test.com`,
  });
});
$ npx vitest

AssertionError: expected { no: 1, email: 'user1@test.com' } to be { no: 1, email: 'user1@test.com' } // Object.is equality

If it should pass with deep equality, replace "toBe" with "toStrictEqual"

Expected: { no: 1, email: 'user1@test.com' }
Received: serializes to the same string


Compared values have no visual difference.

 ❯ eval src/dale.test.ts:11:22
      9|
     10| test('return a user object', () => {
     11|   expect(getUser(1)).toBe({
       |                      ^
     12|     no: 1,
     13|     email: `user1@test.com`,

하지만 toBe() 매처 함수 대신에 toEqual() 매처 함수를 사용하면 테스트는 통과할 것입니다.

import { expect, test } from "vitest";

test("return a user object.", () => {
  expect(getUser(1)).toEqual({
    no: 1,
    email: `user1@test.com`,
  });
});

이렇게 toEqual() 매처 함수는 메모리 주소가 아닌 객체의 실제 모습을 기준으로 검증을 해줍니다. 자바스크립트에서 얼마나 객체를 많이 사용하는지를 생각해보면 테스트를 작성할 때 toBe()보다는 toEqual()를 더 자주 사용하게 될 거라는 것을 짐작할 수 있습니다.

toMatchObject(), toHaveProperty()

큰 객체를 테스트할 때 모든 속성까지 검증할 필요는 없고 일부 속성만 검증하고 싶을 때가 있는데요. 이럴 때는 toMatchObject()toHaveProperty() 매처 함수를 유용하게 사용할 수 있습니다.

toMatchObject() 매처 함수는 인자로 넘어온 객체가 검증 대상 객체의 일부인지를 테스트할 때 사용합니다. toHaveProperty() 매처 함수로는 개별 속성이 검증 대상 객체에 존재하는지 테스트할 수 있으며, 속성 이름만 검사할 수도 있고, 속성 이름과 값을 모두 확인할 수도 있습니다.

import { expect, test } from "vitest";

test("object", () => {
  const user = {
    no: 1,
    email: "john.doe@test.com",
    firstName: "John",
    lastName: "Doe",
  };
  expect(user).toMatchObject({ firstName: "John", lastName: "Doe" });
  expect(user).toHaveProperty("firstName", "John");
  expect(user).toHaveProperty("lastName");
});

toHaveLength(), toContain()

배열의 경우에는 배열이 길이를 체크하거나 특정 원소가 배열에 들어있는지를 테스트해야 할 때가 많은데요. toHaveLength() 매처 함수는 배열의 길이를 체크할 때 쓰이고, toContain() 매처 함수는 특정 원소가 배열에 들어있는지를 테스트할 때 쓰입니다.

import { expect, test } from "vitest";

test("array", () => {
  const colors = ["Red", "Yellow", "Blue"];
  expect(colors).toHaveLength(3);
  expect(colors).toContain("Yellow");
  expect(colors).not.toContain("Green");
});

주의할 점은 배열에 원시형 값이 아니라 객체가 저장되어 있다면 toContain() 대신에 toContainEqual()을 사용해야합니다.

import { expect, test } from "vitest";

test("array", () => {
  const colors = [{ color: "Red" }, { color: "Yellow" }, { color: "Blue" }];
  expect(colors).toContainEqual({ color: "Yellow" });
});

이유는 위에서 설명드린 toBe() 대신에 toBeEqual()을 사용해야 하는 이유와 동일하다고 보시면 됩니다.

toMatch()

문자열의 경우에는 단순히 toBe()를 사용해서 문자열이 정확히 일치하는지를 체크하지만, 종종 정규식 기반의 테스트가 필요할 떄가 있는데요. 이럴 때는 toMatch() 함수를 사용하면됩니다.

import { expect, test } from "vitest";

test("string", () => {
  expect(getUser(1).email).toBe("user1@test.com");
  expect(getUser(2).email).toMatch(/^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/);
});

toBeUndefined(), toBeNull()

자바스크립트는 특이하게 값이 누락된 것을 나타내기 위해서 undefinednull, 이렇게 다른 2개의 원시형 값을 사용하는데요. 테스트를 작성하다보면 함수나 클래스의 메서드가 undefined 또는 null을 반환해야하는지 확인해야할 때가 은근히 많습니다.

예를 들어, users 배열에 저장되어 있는 사용자를 찾아주는 findUser() 함수를 구현해보겠습니다.

const users = [
  { no: 1, email: `user1@test.com` },
  { no: 2, email: `user2@test.com` },
  { no: 3, email: `user3@test.com` },
];

function findUser(no: number) {
  return users.find((user) => user.no === no);
}

findUser() 함수는 users 배열에 존재하는 사용자 번호가 인자로 넘어오면 사용자 객체를 반환하고, 존재하지 않는 사용자 번호가 인자로 넘어오면 undefined를 반환합니다.

이 함수에 대신 테스트는 toBeDefined()toBeUndefined() 매처 함수를 사용하여 쉽게 작성할 수 있습니다.

import { expect, test } from "vitest";

test("finds a user object.", () => {
  expect(findUser(1)).toBeDefined();
  // 또는
  expect(findUser(1)).not.toBeUndefined();
});

test("returns undefined if no user is found.", () => {
  expect(findUser(999)).toBeUndefined();
});

제가 만약에 findUser() 함수가 사용자가 없을 시 null 반환하도록 구현을 살짝 바꾼다면, toBeUndefined() 매처 함수 대신에 toBeNull() 매처 함수를 사용해야 할 것 입니다.

function findUser(no: number) {
  return users.find((user) => user.no === no) ?? null;
}
test("returns null if no user is found.", () => {
  expect(findUser(999)).toBeNull();
});

toBeTruthy(), toBeFalsy()

느슨한 타이핑(typing) 언어인 자바스크립트는, 자바같은 강한 타이핑 기반 언어처럼 truefalse가 불리언(Boolean) 타입에 한정되지 않습니다. 예를 들어, 숫자 1true로 간주되고, 숫자 0false로 간주되는 등 모든 타입의 값들이 true 아니면 false 간주됩니다. 이 밖에도 빈 문자열은 false로 간주되는데, 빈 객체와 배열은 true로 간주되는 등 변환 규칙이 좀 햇갈리죠. 😵‍💫

그래서 테스트를 작성하다보면 toBe(true)toBe(false)로는 부족할 때가 있는데요. 이 때 사용하면 편리한 매처 함수가 바로 toBeTruthy()toBeFalsy()입니다.

toBeTruthy()는 검증 대상이 true로 간주되면 검증이 통과되고, toBeFalsy()는 반대로 false로 간주되는 경우 검증이 통과됩니다.

import { expect, test } from "vitest";

test("number 0 is falsy but string 0 is truthy", () => {
  expect(0).toBeFalsy();
  expect("0").toBeTruthy();
});

toBeTypeOf(), toBeInstanceOf()

toBeTypeOf()toBeInstanceOf()는 각각 테스트 대상의 자료형과 프로토타입 체인을 검사하기 위해서 사용됩니다.

import { expect, test } from "vitest";

test("typeof", () => {
  expect(1).toBeTypeOf("number");
  expect("1").toBeTypeOf("string");
});

test("instanceof", () => {
  const date = new Date();
  expect(date).toBeInstanceOf(Date);
  expect(date).toBeInstanceOf(Object);
});

이 두 개의 매처 함수를 이해하기 위해서는 우선 자바스크립트의 typeof 연산자와 instanceof 연산자가 어떻게 작동하는지 이해하는 것이 필요합니다. 이 부분에 대해서는 아래 두 개의 포스팅에서 자세히 다루고 있으니 참고 바랍니다.

toThrowError()

마지막으로 예외 발생 여부를 테스트해야할 때는 toThrowError() 매처 함수를 사용합니다.

위에서 작성한 getUser() 함수가 음수 아이디가 들어왔을 경우, 오류를 던지도록 수정해보겠습니다.

function getUser(no: number) {
  if (no < 0) throw new Error("Invalid User Number");
  return {
    no,
    email: `user${no}@test.com`,
  };
}

그리고 테스트 코드를 작성해서 실행해보면 다음과 같이 테스트가 실패하게 됩니다.

import { expect, test } from "vitest";

test("throw when id is non negative", () => {
  expect(getUser(-1)).toThrowError();
});
 FAIL  src/dale.test.ts [ src/dale.test.ts ]
Error: Invalid User Number
 ❯ getUser src/dale.test.ts:2:22
      1| function getUser(no: number) {
      2|   if (no <= 0) throw new Error('Invalid User Number');
       |                      ^
      3|   return {
      4|     no,
 ❯ eval src/dale.test.ts:15:8

toThrowError() 매처 함수를 사용할 때 하기 쉬운 실수 인데요. 반드시 expect() 함수에 넘기는 검증 대상을 함수로 한 번 감싸줘야 합니다. 그렇지 않으면 예외 발생 여부를 체크하는 것이 아니라, 테스트 실행 도중 정말 그 예외가 발생하기 때문에 그 테스트는 항상 실패하게 됩니다.

아래와 같이, 예외가 발생할 함수 호출 부분을 함수로 감싸줘야 테스트가 통과할 것입니다.

import { expect, test } from "vitest";

test("throw when id is non negative", () => {
  expect(() => getUser(-1)).toThrowError();
});

toThrowError() 매처 함수를 사용하여 단순히 오류 발생 여부 뿐만 아니라 오류 메시지나 오류 타입까지도 검증할 수 있습니다. 정규식도 지원합니다.

import { expect, test } from "vitest";

test("throw when id is non negative", () => {
  expect(() => getUser(-1)).toThrowError("Invalid User Number");
  expect(() => getUser(-1)).toThrowError(/invalid/i);
  expect(() => getUser(-1)).toThrowError(new Error("Invalid User Number"));
});

매처 함수의 필요성

간혹 toBe(true) 또는 toBe(false)만 쓰면 되는데 뭐하라 이 많은 매처 함수를 학습해야하는지 의구심이 드실 수도 있습니다. 예를 들어, toEqual() 매처 함수는 JSON.stringify() 함수를 사용하면 다음과 같이 어렵지 않게 직접 구현할 수도 있죠.

import { expect, test } from "vitest";

test("toBe", () => {
  const obj1 = { a: 1, b: 2 };
  const obj2 = { a: 1, b: 3 };
  expect(JSON.stringify(obj1) === JSON.stringify(obj2)).toBe(true);
});

그런데 이렇게 작성한 테스트 코드는 테스트가 통과할 때는 문제가 되지 않는데, 테스트가 실패했을 때 Vitest에서 많은 정보를 줄 수가 없습니다. 따라서 디버깅은 오로지 개발자의 몫이지요.

 FAIL  src/dale.test.ts > toBe
AssertionError: expected false to be true // Object.is equality

- Expected
+ Received

- true
+ falseeval src/dale.test.ts:6:57
      4|   const obj1 = { a: 1, b: 2 };
      5|   const obj2 = { a: 1, b: 3 };
      6|   expect(JSON.stringify(obj1) === JSON.stringify(obj2)).toBe(true);
       |                                                         ^
      7| });
      8|

하지만 toEqual() 매처 함수를 사용하면 정확히 어떤 속성이 다른지 Vitest에서 피드백을 주기 때문에 디버깅이 훨씬 수월해집니다.

import { expect, test } from "vitest";

test("toEqual", () => {
  const obj1 = { a: 1, b: 2 };
  const obj2 = { a: 1, b: 3 };
  expect(obj1).toEqual(obj2);
});
 FAIL  src/dale.test.ts > toEqual
AssertionError: expected { a: 1, b: 2 } to deeply equal { a: 1, b: 3 }

- Expected
+ Received

  Object {
    "a": 1,
-   "b": 3,
+   "b": 2,
  }eval src/dale.test.ts:6:16
      4|   const obj1 = { a: 1, b: 2 };
      5|   const obj2 = { a: 1, b: 3 };
      6|   expect(obj1).toEqual(obj2);
       |                ^
      7| });
      8|

검증 목적에 맞는 매처 함수를 사용하면 테스트 코드가 읽기 쉬워지는 부분은 굳이 제가 따로 말씀 안드려도 충분히 느끼실 것입니다.

마치면서

이상으로 Vitest에서 제공하는 다양한 매처 함수를 사용해서 기본적인 테스트 코드를 작성하는 방법에 대해서 알아보았습니다. 본 포스팅에서 정리한 매처 함수들의 사용법이 Vitest로 테스트를 작성하실 때 도움이 되었으면 좋겠습니다.

Vitest에 연관된 포스팅은 Vitest 태그를 통해서 쉽게 만나보세요!