Logo

Jest로 테스트 전/후 처리하기

테스트를 작성하다보면 모든 테스트 함수에서 공통적으로 필요한 공통 로직이 필요할 때가 있습니다. 이번 포스팅에서는 Jest를 이용해서 이렇게 테스트 전이나 후에 항상 실행되야 하는 코드를 작성하는 방법에 대해서 알아보겠습니다.

테스트 대상 코드

데이터베이스에 접근하는 코드에 대한 테스트를 작성한다는 가정 하에, 다음과 같이 간단한 예제 코드를 작성해보겠습니다.

임의의 데이터베이스 역할을 하는 모듈로서 사용자 데이터를 저장하기 위한 users 배열을 가지고 있습니다.

src/data.js
module.exports = {
  users: [],
};

data 모듈에 저장되어 있는 users 배열에 사용자 데이터를 조회/생성/삭제/변경하는 기능을 제공하는 모듈입니다.

src/userService.js
const data = require("./data");

module.exports = {
  findAll() {
    return data.users;
  },

  create(user) {
    data.users.push(user);
  },

  destroy(id) {
    data.users.splice(
      data.users.findIndex((user) => user.id === id),
      1
    );
  },

  update(id, user) {
    data.users[data.users.findIndex((user) => user.id === id)] = user;
  },
};

자바스크립트 배열의 push() 메서드에 대한 좀 더 자세한 설명은 관련 포스팅를 참고바랍니다.

userService.findAll() 테스트

먼저 userService 모듈의 findAll() 함수에 대한 테스트부터 작성해보겠습니다. data 모듈의 users 배열이 비어있으므로 세 건의 데이터를 추가 후에 userService.findAll()를 호출합니다. 그 다음, 리턴 결과의 길이가 3인지, 그리고 각 사용자 데이터를 정확히 담고 있는지 검증합니다.

const userService = require("./userService");
const data = require("./data");

test("find all users", () => {
  data.users.push(
    { id: 1, email: "user1@test.com" },
    { id: 2, email: "user2@test.com" },
    { id: 3, email: "user3@test.com" }
  );

  const users = userService.findAll();

  expect(users).toHaveLength(3);
  expect(users).toContainEqual({ id: 1, email: "user1@test.com" });
  expect(users).toContainEqual({ id: 2, email: "user2@test.com" });
  expect(users).toContainEqual({ id: 3, email: "user3@test.com" });
});

userService.create() 테스트

다음으로 userService 모듈의 create() 함수에 대한 테스트를 작성해보겠습니다. userService.create() 함수를 사용자 한 건의 데이터를 인자로 넘겨서 호출합니다. 그 다음, data 모듈이 데이터를 총 한 건을 저장하고 있고, 정확한 사용자 데이터를 저장하고 있는지 검증합니다.

test("create a user", () => {
  const user = { id: "4", email: "user4@test.com" };

  userService.create(user);

  expect(data.users).toHaveLength(1);
  expect(data.users).toContainEqual(user);
});

하지만 위 테스트를 실행해보면 실제 data 모듈의 users 배열의 길이가 4이었기 때문에 실패함을 알 수 있습니다.

$ jest userService
 FAIL  src/userService.test.js
  ✓ find all users (3ms)
  ✕ create a user (2ms)

  ● create a user

    expect(received).toHaveLength(expected)

    Expected length: 1
    Received length: 4
    Received array:  [{"email": "user1@test.com", "id": 1}, {"email": "user2@test.com", "id": 2}, {"email": "user3@test.com", "id": 3}, {"email": "user4@test.com", "id": "4"}]

      22 |   userService.create(user);
      23 |
    > 24 |   expect(data.users).toHaveLength(1);
         |                      ^
      25 |   expect(data.users).toContainEqual(user);
      26 | });
      27 |

      at Object.toHaveLength (src/userService.test.js:24:22)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 passed, 2 total
Snapshots:   0 total
Time:        0.996s, estimated 1s

왜 이런 상황이 발생하는 것일까요? 그 이유는 첫번째 테스트에서 data 모듈의 users 배열에 데이터를 추가했던 것이 두번째 테스트에 영향을 주기 때문입니다.

afterEach()로 데이터 정리하기

위와 같은 상황을 피하기 위해서는 각 테스트를 실행 후에 data 모듈에 저장되어 있는 데이터를 정리해주는 작업이 필요합니다. Jest의 afterEach() 함수의 인자로 데이터를 정리해주는 코드를 넘겨주면 됩니다.

afterEach(() => {
  data.users.splice(0);
});

afterEach() 함수로 넘긴 코드는 첫번째 테스트가 실행된 후 호출 되고, 두번째 테스트가 실행된 후에도 호출되기 때문에 위에 테스트는 통과하게 됩니다.

자바스크립트 배열의 splice() 메서드에 대한 좀 더 자세한 설명은 관련 포스팅를 참고바랍니다.

userService.destroy() 테스트

다음으로 userService 모듈의 destroy() 함수에 대한 테스트를 작성해보겠습니다. findAll() 함수에서 했던 것과 비슷하게 data 모듈에 초기 데이터를 적재해주고, 한 건의 사용자 데이터가 삭제되는지 확인합니다.

test("destroy a user", () => {
  data.users.push(
    { id: 1, email: "user1@test.com" },
    { id: 2, email: "user2@test.com" },
    { id: 3, email: "user3@test.com" }
  );

  const id = 3;
  const user = data.users.find((user) => user.id === id);

  userService.destroy(id);

  expect(data.users).toHaveLength(2);
  expect(data.users).not.toContainEqual(user);
});

위 테스트 코드와 findAll() 함수에 대한 테스트 코드를 비교해보면 data 모듈에 초기 데이터를 적재하는 코드를 중복해서 작성하고 있습니다.

beforeEach()로 중복 코드 제거하기

여러 테스트에 걸쳐 중복된 코드를 작성하는 것은 유지보수를 어렵게 합니다. 따라서 초기 데이터를 적재하는 코드만 추출하여 Jest의 beforeEach() 함수의 인자로 넘기겠습니다.

beforeEach(() => {
  data.users.push(
    { id: 1, email: "user1@test.com" },
    { id: 2, email: "user2@test.com" },
    { id: 3, email: "user3@test.com" }
  );
});

beforeEach() 함수의 인자로 넘어간 코드는 각각의 테스트 함수가 실행되기 전에 매번 실행됩니다. 따라서, userService.findAll()userService.destroy() 함수에 대한 테스트는 다음과 같이 간결해집니다.

test("find all users", () => {
  const users = userService.findAll();

  expect(users).toHaveLength(3);
  expect(users).toContainEqual({ id: 1, email: "user1@test.com" });
  expect(users).toContainEqual({ id: 2, email: "user2@test.com" });
  expect(users).toContainEqual({ id: 3, email: "user3@test.com" });
});

test("destroy a user", () => {
  const id = 3;
  const user = data.users.find((user) => user.id === id);

  userService.destroy(id);

  expect(data.users).toHaveLength(2);
  expect(data.users).not.toContainEqual(user);
});

beforeAll(), afterAll()

Jest에는 beforeEach(), afterEach()와 매우 유사하게 생긴 beforeAll(), afterAll() 함수도 있습니다. 함수 이름에 유추할 수 있듯이 beforeAll(), afterAll()은 각각 함수의 전 후에 매번 호출되는 것이 아니라, 맨 처음과 맨 끝에 딱 한 번씩만 호출됩니다.

대표적인 사용 사례로 데이터베이스에 접속할 필요한 연결(Connection) 객체를 생각해볼 수 있습니다. 테스트 함수 마다 매번 Connection을 맺고 끊는 것 보다는 맨 처음에 한 번 Connection을 맺어 놓고 여러 함수에 걸쳐서 사용 한 후 마지막에 Connection을 종료하는 것이 효율적일 것입니다.

let connection;

beforeAll(() => {
  connection = openConnection({ host: "...", port: "..." });
});

afterAll(() => {
  connection.close();
});

only(), skip()

테스트 코드를 디버깅할 때 유용한 함수인 only(), skip()에 대해서 짧게 소개해드리겠습니다. 테스트 파일 안에 테스트 함수에 많은 데 그 중에서 하나만 실패했을 경우, 그 함수만 단독으로 실행해보고 싶을 떄가 있습니다. 그럴 때는 해당 테스트 함수 뒤에 .only() 라고 붙여 주면 Jest Runner는 해당 테스트 파일을 실행할 때 .only()가 붙은 함수만 실행해줍니다.

test.only("run only", () => {
  // 이 테스트 함수만 실행됨
});

test("not run", () => {
  // 실행 안됨
});

skip()only()에 반대로 작동합니다. 어떤 함수만 빼고 실행해보고 싶을 때 해당 테스트 함수에 only()를 붙여주면 Jest Runner는 해당 함수를 제외하고 다른 테스트 함수들만 실행해줍니다.

test.skip("skip", () => {
  // 이 테스트 함수는 제외됨
});

test("run", () => {
  // 실행됨
});

describe(), it()

테스트 파일에 많은 수의 테스트 함수가 작성되어 있는 경우, 연관된 테스트 함수들끼리 그룹화해놓으면 코드를 읽기가 좋습니다. 다음과 같이 Jest의 describe() 함수를 통해 여러 개의 테스트 함수를 묶는 것이 가능합니다.

describe("group 1", () => {
  test("test 1-1", () => {
    // ...
  });

  test("test 1-2", () => {
    // ...
  });
});

describe("group 2", () => {
  it("test 2-1", () => {
    // ...
  });

  it("test 2-2", () => {
    // ...
  });
});

여기서 test() 함수 대신에 it() 함수를 사용하기도 했는데요. 이 두 함수는 완전히 동일한 기능을 하는 함수입니다. 기존 많이 사용되었던 Mocha나 Jasmin 같은 테스트 라이브러리에서 함수명을 it()을 사용하였기 때문에, Jest에서도 it()test() 함수의 별칭으로 제공해주고 있습니다.

마치면서

이상으로 Jest를 이용해서 여러 테스트에 걸쳐서 공통으로 필요한 코드를 작성하는 방법에 대해서 알아보았습니다. 포스팅에서 작성한 전체 코드는 다음 링크를 통해서 확인해보실 수 있으십니다.

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