Logo

[자바스크립트] 테스트 데이터 생성

지난 포스팅에서 가짜 데이터를 만들 때 사용하는 Faker.js에 대해서 간단히 알아보았는데요. 이번 포스팅에서는 실제로 테스트를 작성할 때 Faker.js를 어떻게 활용할 수 있는지에 대해서 다뤄보려고 합니다.

테스트 대상 코드 작성하기

먼저 테스트 대상이 될 임의의 함수를 하나 필요한데요. 사용자 객체를 인자로 받아 회원 가입을 처리해주는 함수를 작성해보겠습니다.

auth.js
import faker from "faker";

export function signUp(user) {
  if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(user.email)) {
    return { error: "이메일 형식에 맞지 않은 메일 주소입니다." };
  }

  if (user.password.length < 8) {
    return { error: "비밀번호는 8자 이상이어야 합니다." };
  }

  // 데이터베이스에 사용자 데이터를 저장

  return { user: { ...user, id: faker.random.number() } };
}

이 함수는 인자로 넘어온 사용자의 이메일과 비밀번호에 대한 입력값 검증이 실패할 경우 error 속성에 오류 메시지를 담아서 반환하며, 회원 가입이 성공한 경우 user 속성에 저장된 사용자 객체를 담아서 반환합니다. 데이터베이스에 사용자 데이터를 저장하는 코드는 본 포스팅에서 중요하지 않는 부분이므로 생략하고 주석 처리만 하였습니다.

테스트 코드 작성하기

위 회원 가입 함수를 3가지 케이스에서 호출하는 테스트 코드를 작성해보았습니다. 첫번째 케이스는 회원 가입이 성공한 경우, 두번째 케이스는 이메일 검증이 실패한 경우, 세번째 케이스는 비빌번호 검증이 실패한 경우 입니다.

auth1.test.js
import { signUp } from "./auth";

test("returns user with id", () => {
  const user = {
    firstName: "길동",
    lastName: "홍",
    email: "test@email.com",
    password: "FgjQkKPy_jeezmQ",
    phone: "01-2345-6789",
    address: "한누리대로 411"
  };

  const results = signUp(user);

  expect(results.user).toEqual(
    expect.objectContaining({
      id: expect.any(Number)
    })
  );
});

test("returns error given invalid email", () => {
  const user = {
    firstName: "길동",
    lastName: "홍",
    email: "invalid@@email.com",
    password: "FgjQkKPy_jeezmQ",
    phone: "01-2345-6789",
    address: "한누리대로 411"
  };

  const results = signUp(user);

  expect(results.error).toBe("이메일 형식에 맞지 않은 메일 주소입니다.");
});

test("returns error given too short password", () => {
  const user = {
    firstName: "길동",
    lastName: "홍",
    email: "test@email.com",
    password: "12345",
    phone: "01-2345-6789",
    address: "한누리대로 411"
  };

  const results = signUp(user);

  expect(results.error).toBe("비밀번호는 8자 이상이어야 합니다.");
});

딱 보았을 때 위 테스트 코드는 상당히 읽기가 어려답다는 것을 알 수 있습니다. 정확히 사용자 객체의 어느 속성값 때문에 입력값 검증이 실패하는지 주의깊게 보지 않으면 알기가 어렵습니다.

속성값으로 가짜 데이터 사용하기

지난 포스팅에서 배웠던 Faker.js를 이용해서 테스트 코드의 가독성을 개선시킬 수 있습니다. 해당 테스트 함수에서 중요한 속성에만 진짜 속성값을 사용하고 나머지 속성에는 가짜 속성값을 사용하는 것입니다. 예를 들어, 두 번째 테스트 케이스에서는 email 속성값으로 유효하지 않는 메일 주소를 사용하는 것이 핵심입니다. 따라서 중요하지 않은 나머지 속성에는 Faker.js에서 랜덤으로 생성해준 가짜 데이터를 사용할 수 있습니다.

auth2.test.js
import faker from "faker";
import { signUp } from "./auth";

faker.locale = "ko";

test("returns user with id", () => {
  const user = {
    firstName: faker.name.firstName(),
    lastName: faker.name.lastName(),
    email: faker.internet.email(),
    password: faker.internet.password(),
    phone: faker.phone.phoneNumber(),
    address: faker.address.streetAddress()
  };

  const results = signUp(user);

  expect(results.user).toEqual(
    expect.objectContaining({
      id: expect.any(Number)
    })
  );
});

test("returns error given invalid email", () => {
  const user = {
    firstName: faker.name.firstName(),
    lastName: faker.name.lastName(),
    email: "invalid@@email.com",
    password: faker.internet.password(),
    phone: faker.phone.phoneNumber(),
    address: faker.address.streetAddress()
  };

  const results = signUp(user);

  expect(results.error).toBe("이메일 형식에 맞지 않은 메일 주소입니다.");
});

test("returns error given too short password", () => {
  const user = {
    firstName: faker.name.firstName(),
    lastName: faker.name.lastName(),
    email: faker.internet.email(),
    password: "12345",
    phone: faker.phone.phoneNumber(),
    address: faker.address.streetAddress()
  };

  const results = signUp(user);

  expect(results.error).toBe("비밀번호는 8자 이상이어야 합니다.");
});

이렇게 테스트 코드를 수정해주면 진짜 속상값이 두드러져서 각 테스트의 의도가 무엇인지 좀 더 쉽게 파악할 수 있습니다.

하지만 사용자 객체를 만드는 코드가 3개의 테스트 함수에 중복되는 것을 볼 수 있습니다. 이러한 중복은 테스트 코드의 유지보수성을 크게 해칠 수 있습니다. 만약에 사용자 객체에 신규 속성이 추가되거나, 기존 속성이 변경된다면 모든 테스트 함수를 수정해야줘야 할 것이기 때문입니다.

중복 코드를 팩토리 함수로 빼내기

테스트 데이터를 만들면서 발생하는 중복 코드를 제거하는 가장 효과적인 방법은 해당 코드를 별도의 함수로 빼내는 것입니다. 이렇게 특정 객체를 생성하기 위한 코드를 흔히 객체 팩토리(object factory, object mother)라고 일컽습니다.

사용자 객체를 생성하는 코드를 buildUser()라는 함수로 빼보았습니다. 그리고 각 테스트 케이스에 맞게 일부 속성값을 덮어쓸 수 있도록 overrides 인자를 받도록 하였습니다.

auth3.test.js
import faker from "faker";
import { signUp } from "./auth";

faker.locale = "ko";

function buildUser({ overrides } = {}) {
  return {
    firstName: faker.name.firstName(),
    lastName: faker.name.lastName(),
    email: faker.internet.email(),
    password: faker.internet.password(),
    phone: faker.phone.phoneNumber(),
    address: faker.address.streetAddress(),
    ...overrides
  };
}

test("returns user with id", () => {
  const user = buildUser();

  const results = signUp(user);

  expect(results.user).toEqual(
    expect.objectContaining({
      id: expect.any(Number)
    })
  );
});

test("returns error given invalid email", () => {
  const user = buildUser({ overrides: { email: "invalid@@email.com" } });

  const results = signUp(user);

  expect(results.error).toBe("이메일 형식에 맞지 않은 메일 주소입니다.");
});

test("returns error given too short password", () => {
  const user = buildUser({ overrides: { password: "12345" } });

  const results = signUp(user);

  expect(results.error).toBe("비밀번호는 8자 이상이어야 합니다.");
});

어떤가요? 이제 각 테스트의 코드 길이가 확 줄어들면서 코드가 훨씬 읽기 쉬워졌지요? 추후 사용자 객체의 형태가 변하더라도 buildUser() 함수만 수정하면 될 것이기 때문에 유지보수도 훨씬 수월할 것입니다.

@jackfranklin/test-data-bot 활용하기

실제 자바스크립트 프로젝트에서는 적게는 수십 많게는 수백가지 테스트 객체가 존재하기 마련인데요. 따라서 위와 같이 직접 모든 객체에 대해 테스트 팩토리를 작성하는데는 상당한 시간과 노력이 필요할 것입니다.

다행히도 자바스크립트 커뮤니티에는 이러한 문제를 해결할 수 있도록 @jackfranklin/test-data-bot라는 라이브러리가 있습니다. 이 라이브러리를 개발 의존성으로 설치하면 테스트를 작성할 때 팩토리 함수를 정말 손쉽게 얻을 수 있습니다.

$ npm i -D @jackfranklin/test-data-bot
auth4.test.js
import faker from "faker";
import { build, fake } from "@jackfranklin/test-data-bot";
import { signUp } from "./auth";

faker.locale = "ko";

const buildUser = build({
  fields: {
    firstName: fake((f) => f.name.firstName()),
    lastName: fake((f) => f.name.lastName()),
    email: fake((f) => f.internet.email()),
    password: fake((f) => f.internet.password()),
    phone: fake((f) => f.phone.phoneNumber()),
    address: fake((f) => f.address.streetAddress())
  }
});

test("returns user with id", () => {
  const user = buildUser();

  const results = signUp(user);

  expect(results.user).toEqual(
    expect.objectContaining({
      id: expect.any(Number)
    })
  );
});

test("returns error given invalid email", () => {
  const user = buildUser({ overrides: { email: "invalid@@email.com" } });

  const results = signUp(user);

  expect(results.error).toBe("이메일 형식에 맞지 않은 메일 주소입니다.");
});

test("returns error given too short password", () => {
  const user = buildUser({ overrides: { password: "12345" } });

  const results = signUp(user);

  expect(results.error).toBe("비밀번호는 8자 이상이어야 합니다.");
});

@jackfranklin/test-data-bot 라이브러리는 내부적으로 Faker.js를 사용하고 있기 때문에 API가 친숙하게 느껴질 것입니다. (@jackfranklin/test-data-bot 라이브러리에 대한 좀 더 자세한 내용은 Github 저장소를 참고 바랍니다.)

객체 팩토리 모듈화 하기

마지막으로, 생성해야할 테스트 객체가 많은 프로젝트에서는 테스트 코드를 작성할 때 필요한 모든 팩토리 함수를 하나의 모듈에 모아놓고 관리하는 것을 추천드립니다. 비단 이 테스트 파일 뿐만 아니라 다른 테스트 파일에서도 사용자 객체를 생성해야 될 확률이 매우 높기 때문입니다. 테스트 코드로 부터 객체 팩토리가 분리해놓으면 유지 보수가 더욱 쉬워지고, 팩토리 함수 간에 상호 호출도 용이해지는 장점이 있습니다.

builders.js
import faker from "faker";
import { build, fake } from "@jackfranklin/test-data-bot";

faker.locale = "ko";

export const buildUser = build({
  fields: {
    firstName: fake((f) => f.name.firstName()),
    lastName: fake((f) => f.name.lastName()),
    email: fake((f) => f.internet.email()),
    password: fake((f) => f.internet.password()),
    phone: fake((f) => f.phone.phoneNumber()),
    address: fake((f) => f.address.streetAddress())
  }
});
auth5.test.js
import { buildUser } from "./builders";
import { signUp } from "./auth";

test("returns user with id", () => {
  const user = buildUser();

  const results = signUp(user);

  expect(results.user).toEqual(
    expect.objectContaining({
      id: expect.any(Number)
    })
  );
});

test("returns error given invalid email", () => {
  const user = buildUser({ overrides: { email: "invalid@@email.com" } });

  const results = signUp(user);

  expect(results.error).toBe("이메일 형식에 맞지 않은 메일 주소입니다.");
});

test("returns error given too short password", () => {
  const user = buildUser({ overrides: { password: "12345" } });

  const results = signUp(user);

  expect(results.error).toBe("비밀번호는 8자 이상이어야 합니다.");
});

전체 코드

본 포스팅에서 작성한 모든 테스트 코드는 아래에서 확인하고 직접 실행해보실 수 있습니다.

마치면서

지금까지 테스트 코드를 리팩토링(refactoring)하면서 어떻게 하면 좀 더 효과적으로 테스트 데이터를 생성할 수 있는지 알아보았습니다. 본 포스팅에서 다룬 테스팅 기법을 잘 활용하셔서 유지 보수하기 편하고 읽기 쉬운 테스트 코드를 작성하실 수 있으시기를 바래봅니다.