Article

테스트 데이터는 Fixture와 Factory 중 무엇으로 만들까

작성: 감성개발자

테스트 데이터는 Fixture와 Factory 중 무엇으로 만들까

Fixture와 Factory 기반 테스트 데이터 구조를 추상적으로 표현한 삽화

전체 테스트를 돌리면 실패하는데, 그 테스트 하나만 따로 돌리면 통과합니다. 열어 보면 대개 이런 코드가 있습니다.

// user_17은 이 파일이 아니라 어딘가의 공용 fixture가 만든 사용자
const order = createOrder({ userId: 17 });
expect(order.status).toBe("paid");

user_17이 누구인지, 왜 이 주문이 paid여야 하는지 이 테스트 안에는 없습니다. 조건은 전부 다른 파일에 흩어져 있고, 그 파일을 누군가 건드리는 순간 여기가 무너집니다. 테스트가 깨졌는데 원인이 코드인지 데이터인지 모르면, 그 테스트는 이미 절반쯤 빚이 됩니다.

이 테스트가 흔들리는 이유는 데이터 관리 방식이 하나로 뭉쳐 있어서입니다. 그래서 나는 테스트 데이터를 한 가지 방식으로 통일하기보다 목적별로 나누는 쪽을 기본으로 잡습니다.

변하지 않는 기준 데이터는 Fixture와 seed로 작게 묶고, 케이스마다 달라지는 사용자·주문·결제는 Factory나 Builder로 테스트 바로 옆에서 만드는 것이 출발점입니다. 로그인 쿠키와 세션이 담기는 E2E 인증 상태 파일은 테스트 데이터인 동시에 민감 정보라, .gitignore와 만료·재생성 절차를 함께 정해야 합니다.

이 글은 Jest, Playwright, Laravel Factory 문서와 Martin Fowler의 Test Data Builder 설명을 바탕으로 삼았습니다. 특정 언어나 프레임워크 하나에 묶기보다, 테스트 데이터가 커질 때 유지보수 비용을 줄이는 판단 기준에 초점을 맞췄습니다. (최종 업데이트: 2026-07-14)

테스트 데이터가 테스트보다 커지는 순간을 먼저 잡아야 합니다

하나의 공유 데이터 덩어리에서 여러 테스트가 실처럼 뻗어 나와 매달려 있고 그 중심이 한 곳에서 흔들리자 균열이 모든 실을 타고 번져 연결된 테스트들이 동시에 깨지는 모습. 공유 데이터에 기댄 테스트들이 한 번의 변화로 함께 무너지는 실패 구조를 보여준다.

작은 프로젝트에서는 테스트 데이터가 별문제처럼 보이지 않습니다. 회원 2명, 상품 3개, 주문 1개 정도면 파일 하나에 넣고 시작해도 됩니다. 문제는 테스트가 늘어난 뒤입니다. 전역 Fixture에 새 컬럼이 붙고, 테스트마다 같은 데이터를 조금씩 다르게 해석하기 시작하면 실패 원인을 찾기 어려워집니다.

에이전시 시절 유지보수로 넘겨받은 프로젝트마다 이 패턴이 반복됐습니다. 처음 만든 사람은 편하게 파일 하나로 시작했는데, 몇 달 뒤에는 아무도 그 Fixture를 건드리지 못하는 상태가 되어 있었습니다. 나는 그 뒤로 테스트 데이터를 “어디에 둘까”보다 “누가 언제 바꾸는가”를 먼저 따지게 됐습니다.

테스트 데이터 관리에서 가장 먼저 피해야 할 것은 “어딘가에 이미 있는 데이터”에 기대는 습관입니다. 테스트는 읽는 사람이 조건을 바로 알아야 합니다. 어떤 사용자가 휴면 상태인지, 어떤 주문이 환불 완료인지, 왜 이 권한이 필요한지 본문 근처에서 드러나야 합니다.

실무에서는 아래처럼 나눠 보는 게 출발점입니다.

데이터 성격추천 방식피해야 할 사용법
국가 코드, 고정 카테고리, 권한 목록Fixture 또는 seed테스트마다 수정되는 공유 데이터로 쓰기
사용자, 주문, 결제, 게시글처럼 케이스별 조건이 다른 데이터Factory 또는 Builder거대한 공통 Fixture에서 골라 쓰기
브라우저 로그인 상태, 세션 쿠키테스트 전용 인증 상태 파일저장소에 커밋하거나 개인 계정으로 재사용
외부 API 응답Mock, stub, contract fixture운영 API에 직접 의존

핵심은 “편한 생성”보다 “읽히는 조건”입니다. 테스트 데이터가 많아도 테스트 목적이 선명하면 괜찮습니다. 반대로 데이터가 적어도 조건이 숨어 있으면 유지보수가 어려워집니다.

Fixture가 맞는 데이터는 생각보다 좁습니다

Fixture는 미리 정해 둔 데이터를 테스트 전에 불러오는 방식입니다. 파일로 관리하기 쉽고, 사람이 눈으로 확인하기 좋습니다. 그래서 초기에는 가장 편합니다. 하지만 모든 테스트 데이터를 Fixture로 밀어 넣으면 곧 전역 상태가 됩니다.

Fixture가 잘 맞는 경우는 보통 변하지 않는 기준 데이터입니다.

  • ISO 국가 코드, 통화 코드처럼 서비스 규칙상 거의 고정된 값
  • 역할 이름, 권한 목록, 기본 설정값
  • 외부 API의 대표 응답 예시처럼 스키마 검증에 쓰는 샘플
  • 마이그레이션 이후 반드시 있어야 하는 seed 성격의 데이터

반대로 사용자, 주문, 결제 같은 도메인 객체는 Fixture 하나로 오래 버티기 어렵습니다. 테스트마다 “일반 사용자”, “정지된 사용자”, “이메일 미인증 사용자”, “구독 만료 사용자”가 필요해지고, 결국 같은 파일 안에서 서로 다른 케이스가 뒤섞입니다. 나는 도메인 객체를 전역 Fixture에 넣자는 제안이 나오면 일단 멈추자고 말하는 편입니다. 당장은 코드가 줄지만, 그 파일을 나중에 나눠야 할 때 드는 비용이 훨씬 큽니다.

Jest 문서도 테스트 전후 준비 작업을 beforeEach, afterEach, beforeAll, afterAll로 나눠 관리할 수 있다고 설명합니다. 이 말은 데이터를 어디에나 올려 두라는 뜻이 아니라, 테스트마다 필요한 준비 범위를 구분하라는 쪽에 가깝습니다. 전체 테스트에 필요한 데이터인지, 각 테스트마다 새로 만들어야 하는 데이터인지 먼저 나누는 편이 안전합니다. (출처: Jest - Setup and Teardown)

Factory는 케이스마다 달라지는 조건을 드러낼 때 좋습니다

Factory는 필요한 객체를 테스트 안에서 만들어내는 방식입니다. Laravel 문서의 Eloquent Factory처럼 기본 속성을 정의해 두고, state나 sequence를 이용해 특정 상태의 모델을 만들 수 있습니다. 즉 “기본 사용자를 하나 만들되, 이 테스트에서는 admin 상태로 만든다” 같은 표현이 쉬워집니다. (출처: Laravel Docs - Eloquent Factories)

Factory의 장점은 테스트가 자기 데이터를 직접 설명한다는 점입니다.

const user = createUser({ role: "admin", emailVerified: true });
const order = createOrder({ userId: user.id, status: "paid" });

이 코드는 길지 않지만 조건이 보입니다. 전역 Fixture에서 user_17order_42를 가져오는 방식보다 읽는 사람이 덜 헤맵니다. 실패했을 때도 이 테스트가 만든 데이터부터 보면 됩니다.

다만 Factory도 방치하면 또 다른 혼란이 됩니다. 모든 필드에 랜덤값을 넣고, 테스트마다 생성 결과가 달라지면 재현성이 떨어집니다. Faker 같은 도구를 쓰더라도 테스트에서 중요한 값은 명시하고, 나머지만 기본값으로 감추는 편이 좋습니다.

좋은 Factory 사용 기준은 이렇습니다.

  • 테스트 본문에는 중요한 차이만 드러낸다.
  • 랜덤값은 꼭 필요할 때만 쓰고, 실패 재현이 가능하게 seed를 고정한다.
  • 생성 함수가 DB 저장까지 하는지, 객체만 만드는지 이름으로 구분한다.
  • 관계 데이터는 너무 자동으로 만들지 않는다. 필요한 관계만 테스트에서 보이게 둔다.

Factory는 “많이 만드는 도구”가 아니라 “조건을 읽기 쉽게 만드는 도구”에 가깝습니다.

Test Data Builder는 기본값을 숨기고 의도를 남깁니다

Test Data Builder는 Factory와 비슷하지만, 테스트에서 바꾸고 싶은 조건을 메서드 체인이나 작은 조합으로 드러내는 패턴입니다. Martin Fowler는 테스트 데이터를 준비할 때 불필요한 세부값이 테스트를 어지럽힐 수 있고, Builder가 기본값을 제공해 테스트에서 중요한 값만 표현하게 해 준다고 설명합니다. (출처: Martin Fowler - Test Data Builder)

예를 들면 이런 느낌입니다.

const order = new OrderBuilder()
  .withPaidStatus()
  .withTotalAmount(50000)
  .forNewUser()
  .build();

이 방식은 도메인 조건이 많은 서비스에서 특히 좋습니다. 주문에는 사용자, 주소, 상품, 할인, 결제, 배송 상태가 엮입니다. 테스트마다 이 값을 전부 나열하면 본문이 데이터 준비 코드로 덮입니다. Builder를 쓰면 기본값은 숨기고, 이 테스트에서 검증하려는 차이만 남길 수 있습니다.

하지만 Builder도 너무 똑똑해지면 위험합니다. forVipUserWithExpiredCouponAndPartialRefund() 같은 메서드가 늘어나면 오히려 내부 구현을 열어 봐야 합니다. 이름은 짧고, 조합은 작게 유지하는 편이 낫습니다.

E2E 테스트 데이터는 계정과 상태를 따로 봐야 합니다

단위 테스트나 통합 테스트와 달리 E2E 테스트는 브라우저 상태가 끼어듭니다. 로그인 쿠키, localStorage, 세션 만료, 테스트 계정의 서버 상태가 함께 움직입니다. 여기서도 “한 계정으로 다 돌리기”는 처음에는 편하지만 병렬 실행이 들어오면 바로 흔들립니다.

Playwright 문서는 테스트가 독립된 browser context에서 실행되며, 인증 상태를 파일로 저장해 재사용할 수 있다고 설명합니다. 동시에 이 인증 상태 파일에는 민감한 쿠키와 헤더가 들어갈 수 있으므로 저장소에 커밋하지 말라고 경고합니다. (출처: Playwright - Authentication)

E2E 인증 데이터는 아래 기준으로 나누면 실수가 줄어듭니다.

  • 모든 테스트가 같은 계정을 써도 서로 상태를 바꾸지 않는가?
  • 테스트가 서버 상태를 수정한다면 병렬 worker마다 별도 계정이 있는가?
  • 인증 상태 파일이 만료됐을 때 재생성 절차가 있는가?
  • 테스트 계정에 운영 권한이나 실제 결제 권한이 붙어 있지 않은가?
  • 세션 파일이 .gitignore에 들어가 있는가?

로그인 시간을 줄이려고 인증 상태 파일을 쓰는 건 좋은 선택일 수 있습니다. 다만 그 파일은 테스트 데이터이면서 동시에 민감 정보입니다. 일반 Fixture처럼 저장소에 올리는 순간 보안 사고가 될 수 있습니다. 팀에 새 인원이 들어올 때 나는 이 파일을 먼저 짚어 줍니다. 세션 쿠키가 커밋에 섞여 들어가는 실수는 한 번 나면 되돌리기가 번거롭기 때문입니다.

seed 데이터는 테스트 편의가 아니라 시스템 기본값에 가깝습니다

seed 데이터는 애플리케이션이 기본으로 가져야 하는 값을 넣는 데 적합합니다. 예를 들어 기본 관리자 역할, 결제 상태 코드, 공개 카테고리처럼 서비스가 시작될 때 존재해야 하는 값입니다.

테스트 편의를 위해 seed에 모든 케이스를 넣기 시작하면 문제가 생깁니다. 테스트가 seed의 특정 row에 의존하고, 다른 테스트가 그 row를 바꾸고, 실행 순서에 따라 결과가 달라집니다. seed는 “시스템 기본값”으로 좁게 두고, 케이스 데이터는 테스트 안에서 만들도록 분리하는 편이 낫습니다.

판단 기준은 간단합니다.

  • 운영 환경에도 존재해야 하는 값이라면 seed 후보입니다.
  • 특정 테스트 하나 때문에 필요한 값이라면 Factory나 Builder가 맞습니다.
  • 여러 테스트가 공유하지만 변경되면 안 되는 값이라면 읽기 전용 Fixture가 낫습니다.
  • 테스트가 수정해야 하는 값이라면 공유 seed에 두지 않는 편이 안전합니다.

seed를 줄이면 테스트가 조금 길어질 수 있습니다. 대신 실패 원인은 훨씬 가까운 곳에 남습니다.

테스트 데이터 관리 체크리스트

테스트가 늘어나기 시작했다면 아래 항목을 한 번 점검해 보세요. 전부 완벽하게 맞출 필요는 없지만, 많이 걸릴수록 테스트 데이터 구조를 손볼 때입니다.

  • 테스트가 실패했을 때 필요한 데이터 조건이 본문 근처에서 보인다.
  • 전역 Fixture는 변하지 않는 기준 데이터로만 제한했다.
  • 사용자·주문·결제처럼 케이스별로 달라지는 데이터는 Factory나 Builder로 만든다.
  • Factory의 랜덤값은 실패 재현을 방해하지 않는다.
  • seed 데이터는 운영 기본값과 테스트 편의 데이터를 섞지 않는다.
  • E2E 테스트 계정은 병렬 실행과 서버 상태 변경을 고려해 분리했다.
  • 인증 상태 파일, 쿠키, 토큰은 저장소에 올라가지 않는다.
  • 테스트가 끝난 뒤 데이터 정리 방식이 명확하다.

체크리스트의 목적은 테스트를 더 엄격하게 만드는 것이 아닙니다. 테스트가 실패했을 때 원인을 빨리 좁히기 위한 장치입니다.

상황별 추천 기준

작은 프로젝트라면 처음부터 Builder 계층을 크게 만들 필요는 없습니다. 테스트가 몇 개 없고 도메인 조건도 단순하다면 Factory 함수 몇 개로 충분합니다. 오히려 과한 추상화가 테스트를 읽기 어렵게 만들 수 있습니다.

반대로 주문, 권한, 결제, 상태 전이가 많은 서비스라면 Builder나 상태별 Factory를 일찍 도입하는 편이 낫습니다. 테스트마다 20줄짜리 데이터 준비 코드가 반복되기 시작하면, 나중에 고치는 비용이 더 큽니다.

실무 기준으로 정리하면 이렇습니다.

  • 단위 테스트: 객체 Builder나 작은 Factory 함수로 필요한 조건만 만든다.
  • 통합 테스트: DB Factory를 쓰되, 테스트마다 독립 데이터를 만든다.
  • E2E 테스트: 테스트 계정, 인증 상태, 서버 데이터 초기화 절차를 분리한다.
  • 스냅샷·계약 테스트: 대표 응답 Fixture를 쓰되, 운영 데이터처럼 계속 커지지 않게 관리한다.
  • 초기 seed: 애플리케이션 기본값만 넣고, 케이스 데이터는 테스트에서 만든다.

여기서 중요한 건 도구 이름이 아닙니다. 같은 팀 안에서 “무엇은 공유하고, 무엇은 테스트마다 만든다”는 기준이 같아야 합니다. 소규모 팀이라면 이 기준을 문서로 만들 필요도 없습니다. 코드 리뷰에서 한 번씩 맞춰 가면 됩니다. 반대로 인원이 늘고 테스트를 여러 사람이 동시에 손대기 시작하면, 이 기준을 어딘가에 적어 두는 편이 결국 시간을 아낍니다. 도구를 무엇으로 정하든, 테스트가 실패했을 때 데이터 조건을 1분 안에 설명할 수 있으면 그 테스트는 오래 버팁니다.

팀에서 자주 나오는 질문

Fixture를 아예 쓰지 않는 게 좋나요?

아닙니다. 변하지 않는 기준 데이터나 외부 응답 샘플에는 Fixture가 여전히 좋습니다. 문제는 사용자, 주문, 결제처럼 테스트마다 상태가 달라지는 데이터를 큰 Fixture 하나로 공유할 때 생깁니다.

Factory를 쓰면 테스트가 느려지지 않나요?

DB에 실제로 저장하는 Factory가 많아지면 느려질 수 있습니다. 그래서 단위 테스트에서는 객체만 만드는 Factory를 쓰고, 통합 테스트에서만 DB 저장을 사용하는 식으로 나누는 편이 좋습니다. 속도보다 먼저 봐야 할 것은 테스트 독립성과 실패 재현성입니다.

랜덤 테스트 데이터는 쓰면 안 되나요?

쓸 수는 있습니다. 다만 테스트 결과가 달라지면 안 됩니다. 실패를 재현할 수 있도록 seed를 고정하거나, 검증에 중요한 값은 직접 명시하세요. 테스트 본문에서 중요한 값까지 랜덤으로 숨기면 디버깅이 어려워집니다.

E2E 테스트 계정은 하나만 있어도 되나요?

테스트가 서버 상태를 바꾸지 않고 순차 실행만 한다면 하나로 시작할 수 있습니다. 하지만 병렬 실행을 하거나 설정 변경, 주문 생성, 권한 변경처럼 서버 상태를 수정한다면 worker별 계정을 분리하는 편이 안전합니다. Playwright도 공유 계정과 worker별 계정을 상황에 따라 나눠 설명합니다. (출처: Playwright - Authentication)

seed와 Fixture는 무엇이 다른가요?

seed는 애플리케이션이 기본으로 가져야 하는 값에 가깝고, Fixture는 테스트나 검증을 위해 미리 준비한 샘플 데이터에 가깝습니다. 운영에도 필요한 값이면 seed, 특정 테스트를 위한 입력이면 Fixture나 Factory로 보는 편이 덜 헷갈립니다.

출처

댓글