Cloudflare Workers createTestHarness로 프로덕션 빌드 통합 테스트하기
Cloudflare Workers createTestHarness를 쓰면 Vite나 Wrangler로 만든 프로덕션 빌드 산출물을 로컬 서버에 띄우고, 여러 Worker의 route와 외부 API, 브라우저 흐름까지 한 수명주기로 묶을 수 있어요. 핵심 순서는 Vite build → harness 기동 → MSW 외부 요청 차단 → Playwright 연결 → 실패 로그 출력 → reset 및 재시드. 흩어진 공식 문서를 CI gate 하나로 정리하면 딱 이 흐름입니다.
다만 이 글의 코드는 2026년 7월 30일 기준 Cloudflare 공식 문서와 workers-sdk 릴리스를 대조해 구성한 구현안이에요. 실행 가능한 프로젝트 fixture가 없어 직접 돌리거나 성능을 측정한 결과는 아닙니다. “CI에서 통과했음” 같은 후기는 없음. 대신 어디까지 검증할 수 있고 어디서 선을 그어야 하는지 분명하게 잡아볼게요.

먼저 버전과 도구 역할부터 구분하기
createTestHarness()는 2026년 6월 9일 공개된 [email protected]에서 처음 제공됐어요. 2026년 7월 27일 Workers Changelog는 최초 출시 공지가 아니라, 통합 테스트에서 unstable_startWorker()와 unstable_dev() 대신 이 API를 쓰라고 권장한 전환 공지에 가깝습니다. 날짜가 붙어 있으니 은근 헷갈리는 지점.
공식 릴리스와 패키지 요구사항을 기준으로 잡으면 다음과 같습니다.
| 확인 항목 | 기준 |
|---|---|
createTestHarness() 최소 확인 버전 |
Wrangler 4.99.0 |
| Node.js | 22 이상 |
| 조사일의 Wrangler 최신 버전 | 4.115.0 |
| Vite plugin 버전 | 프로젝트의 Vite major와 Wrangler peer range를 lockfile에서 확인 |
실제 프로젝트에서는 latest를 믿고 흘려보내기보다 검증한 정확한 버전을 lockfile에 고정하는 편이 낫습니다. API가 새롭고 요구 버전도 바뀔 수 있으니까요. 게시 직전 공식 릴리스를 한 번 더 보는 게 마음 편-안.
도구 역할도 나눠야 해요. Cloudflare는 Worker runtime 안에서 바인딩 상태를 직접 단언하는 단위 테스트에는 Workers Vitest integration을, Node.js test runner에서 프로덕션 Worker 빌드의 애플리케이션 경계를 확인하는 통합 테스트에는 harness를 제시합니다. 프로그램 방식의 개발 서버가 필요하다면 createTestHarness()가 아니라 Cloudflare Vite plugin과 Vite createServer() API가 맞고요.
Vite 소스 설정이 아니라 빌드 결과를 연결한다
Cloudflare Vite plugin 프로젝트에서 가장 중요한 부분은 configPath예요. 소스의 wrangler.jsonc를 그대로 넘기면 “프로덕션 빌드 테스트”라는 목적과 어긋납니다.
먼저 빌드하고 테스트합니다.
npx vite build
npx vitest run
vite build가 빌드 output에 생성한 dist/<worker>/wrangler.json을 harness가 읽게 해야 해요. static assets가 있다면 생성 config의 assets.directory도 client build output을 가리킵니다.
Wrangler로 관리하는 Worker와 Vite로 빌드한 Worker를 한 배열에 섞는 구성도 가능해요.
import { afterAll, afterEach, beforeAll } from "vitest";
import { createTestHarness } from "wrangler";
const server = createTestHarness({
workers: [
{ configPath: "./workers/api/wrangler.jsonc" },
{ configPath: "./dist/web_worker/wrangler.json" },
],
});
beforeAll(() => server.listen());
afterEach(() => server.reset());
afterAll(() => server.close());
listen()으로 테스트 서버를 한 번 띄우고, 테스트마다 reset(), suite가 끝나면 close()하는 구조입니다. 서버를 매번 새로 만드는 대신 수명주기를 명시적으로 관리함.
여기서 환경 선택도 주의해야 해요. Vite plugin의 CLOUDFLARE_ENV는 harness 실행 시점이 아니라 dev/build 시점에 적용되고, 생성되는 wrangler.json에 평탄화됩니다. production 환경의 구조를 검사하려면 build step에 CLOUDFLARE_ENV=production을 주거나 Vite mode에 맞는 환경 파일을 사용해야 해요. vite preview나 배포 단계에서 뒤늦게 지정해도 효과가 없습니다.
반면 Wrangler source config를 직접 읽는 Worker는 workers[].env: "test"로 환경을 선택할 수 있어요. 비밀값은 저장소나 예제 코드에 넣지 말고 테스트용 placeholder 또는 override를 사용하면 됩니다.

상대 URL과 절대 URL로 다중 Worker route를 나눠 검증한다
workers 배열의 첫 번째 Worker가 primary예요. 그래서 server.fetch("/")처럼 상대 URL을 보내면 primary Worker로 갑니다.
절대 URL은 각 config의 route와 대조해 Worker를 선택하고, 일치하는 route가 없으면 다시 primary로 fallback해요. 다중 Worker 구성을 시험하면서 상대 URL만 호출하면 route 설정 오류를 놓칠 수 있다는 얘기입니다.
const server = createTestHarness({
workers: [
{ configPath: "./dist/web_worker/wrangler.json" },
{ configPath: "./dist/api_worker/wrangler.json" },
],
});
await server.fetch("http://api.example.com/v1/users/123");
await server.fetch("http://example.com/users/123");
route 경계를 검증할 때는 실제 config route와 맞는 절대 URL을 써야 합니다. 특정 Worker 구현이나 scheduled() handler처럼 HTTP route 밖의 event를 확인할 때는 server.getWorker("api-worker")로 직접 handle을 가져오면 되고요.
둘 중 하나만 고집하면 빈틈이 생겨요.
getWorker()만 쓰면 개별 handler는 통과해도 route 설정 오류를 놓칠 수 있음server.fetch()만 쓰면 scheduled handler 같은 비-HTTP 경로를 다루기 어려움
그래서 최소 한 개의 route-dispatch 테스트와, 필요한 직접 event 테스트를 분리해 두는 구성이 자연스럽습니다.
MSW는 외부 HTTP를 막고 binding은 test Worker로 바꾼다
Harness는 Node.js의 globalThis.fetch()를 가로채는 MSW와 함께 사용할 수 있어요. 공식 예제의 중요한 설정은 처리하지 않은 outbound request를 오류로 만드는 것입니다.
network.listen({ onUnhandledRequest: "error" });
허용한 외부 API만 mock 응답을 주고 나머지는 실패시키면, CI가 모킹 누락을 조용히 넘기거나 실수로 실제 외부 서비스에 요청하는 일을 줄일 수 있어요. fail-closed라 조금 빡빡하지만 통합 테스트에서는 오히려 이쪽이 믿음직스러움.
각 테스트 뒤에는 MSW의 network.resetHandlers()와 harness의 server.reset()을 함께 호출하고, suite 종료 시 network.close()와 server.close()를 정리합니다.
HTTP 외부 API가 아니라 Worker binding을 대체해야 할 때는 층이 달라요. bindingOverrides로 Browser Rendering 같은 외부 binding을 harness 내부의 test Worker에 연결할 수 있습니다. 정리하면 이렇습니다.
| 대체 대상 | 권장 계층 |
|---|---|
| Worker가 보내는 외부 HTTP 요청 | MSW |
| Worker binding | bindingOverrides와 test Worker |
| KV, R2, D1, Durable Object 초기 상태 | worker.getEnv()로 binding 접근 |
외부 mock server URL처럼 setup 후에 정해지는 값은 옵션 없이 harness를 만든 뒤 server.update()하고 listen()하는 흐름을 사용할 수 있어요. 실제 운영 token을 테스트 설정에 복사할 이유는 없습니다.
Playwright baseURL과 실패 진단을 같은 fixture에 넣는다
Playwright에서는 worker scope fixture로 harness를 한 번 시작하고, listen()이 반환한 URL을 baseURL로 넘깁니다.
const { url } = await server.listen();
const baseURL = url.href;
이렇게 하면 browser flow가 별도의 임시 dev server가 아니라 harness에 올라간 web/api Worker 구성을 향하게 돼요. MSW와 Playwright, 다중 Worker가 각각 놀지 않고 같은 테스트 서버 수명주기에 들어오는 구조.
실패했을 때는 server.debug()로 server timeline과 캡처된 Worker runtime 로그를 출력할 수 있습니다. 평소에는 로그를 조용히 두고 실패 상태에서만 debug를 실행하면 CI 노이즈도 줄어들어요. 더 세밀하게 다룰 때는 server.getLogs()로 로그를 가져오고 clearLogs()로 구간을 나눌 수 있습니다.
teardown 순서도 은근 중요해요. 테스트 실패를 확인한 뒤 debug 정보를 남기고, 그다음 MSW handler와 Worker 상태를 reset해야 원인 로그를 지워버리지 않습니다. reset() 때 로그도 초기화되거든요.

reset 뒤에는 D1 migration과 seed를 다시 적용한다
server.reset()은 storage를 재생성하고 세션 시작 시의 Worker 옵션을 복원합니다. 테스트 간 격리에는 반가운 동작인데, 준비한 상태도 같이 사라질 수 있어요.
worker.getEnv()로 KV, R2, D1, Durable Object binding에 접근해 초기 데이터를 넣을 수 있고, D1은 worker.applyD1Migrations(bindingName)으로 migration을 적용합니다. 이 메서드는 Wrangler config의 migrations_dir와 migrations_pattern을 읽으며, 별도 옵션이 없다면 config 파일 기준 migrations 폴더의 SQL을 사용해요.
그러므로 각 테스트 준비는 아래 순서로 잡는 편이 안전합니다.
- 이전 테스트의 MSW handler와 harness 상태를 reset한다.
- 필요한 D1 migration을 다시 적용한다.
- 테스트 fixture와 seed data를 다시 넣는다.
- route 또는 browser flow를 실행한다.
- 실패했다면 reset 전에 debug와 runtime log를 남긴다.
reset만 해놓고 다음 테스트를 바로 시작하면 “왜 테이블이 없지??” 상황이 생길 수 있음. 초기화와 재시드는 한 세트입니다.
CI gate는 이 7가지를 완료 조건으로 둔다
공식 예제를 하나의 실행 흐름으로 합치면 script의 골격은 다음처럼 잡을 수 있어요. 아래 버전 범위는 API의 최소 제공 버전을 드러내기 위한 예시이며, 실제 저장소에서는 검증한 정확한 버전과 lockfile을 사용하는 편이 낫습니다.
{
"scripts": {
"build": "vite build",
"test:integration": "vitest run tests/integration",
"test:e2e": "playwright test"
},
"devDependencies": {
"wrangler": ">=4.99.0"
}
}
CI 완료 조건은 명령 실행 자체보다 assertion의 경계를 분명히 두는 게 중요해요.
vite build가 production output과 각 Vite Worker의 생성wrangler.json을 만든다.- Harness가 web/api Worker를 같은 로컬 서버에서 시작한다.
- MSW가 허용한 외부 API만 응답하고 처리하지 않은 outbound fetch는 실패한다.
- API route와 일치하는 절대 URL이 API Worker로 전달되고 계약한 응답을 만족한다.
- Playwright가 harness URL에서 web flow를 실행하고 기대한 UI 상태를 확인한다.
- 각 테스트 뒤 storage, log, option state를 reset하고 migration과 seed를 다시 적용한다.
- 실패 시 debug timeline과 runtime log를 CI log 또는 artifact에서 확인한다.
이 정도면 프로덕션 빌드 산출물과 로컬 통합 경계를 꽤 촘촘하게 확인할 수 있어요. 하지만 Cloudflare 글로벌 네트워크, 실제 remote binding, 계정 권한, 운영 latency까지 재현하는 것은 아닙니다. 로컬 preview를 실제 edge smoke test와 같은 말로 부르면 범위가 너무 커짐.
createTestHarness()의 장점은 새 함수 하나보다도 빌드, route, 외부 요청, 브라우저, 상태 초기화, 실패 진단을 같은 수명주기로 묶는 데 있습니다. 다중 Worker 앱을 배포 전에 걸러내는 CI gate가 필요하다면 써볼 만해요. 대신 배포 뒤의 메모리 거동까지 보장하지는 않으니, 다음 단계로 배포 후 Workers 메모리 회귀 관측 글을 이어서 보면 됩니다.
FAQ
createTestHarness()의 최소 Wrangler와 Node.js 버전은 무엇인가요?
공식 workers-sdk 릴리스 기준 createTestHarness()는 Wrangler 4.99.0에서 도입됐고, Wrangler 4.99.0과 조사일의 4.115.0 패키지는 모두 Node.js 22 이상을 요구합니다. 실제 CI에서는 게시 시점의 공식 릴리스와 lockfile을 다시 확인하세요.
상대 URL과 절대 URL은 어떤 Worker로 전달되나요?
server.fetch("/") 같은 상대 URL은 workers 배열의 첫 번째 primary Worker로 갑니다. 절대 URL은 config route와 대조하며, 일치하는 route가 없으면 primary Worker로 fallback합니다.
server.reset() 뒤에 무엇을 다시 준비해야 하나요?
reset()은 storage를 재생성하고 Worker 옵션을 세션 시작 상태로 복원하며 로그도 초기화합니다. D1 migration과 seed data처럼 테스트가 의존하는 상태는 다음 테스트 setup에서 다시 적용해야 해요.
실제 Cloudflare 계정이나 API token 없이 실행할 수 있나요?
공식 문서가 설명하는 harness는 프로덕션 빌드 output을 로컬 preview 서버에서 실행합니다. 외부 HTTP는 MSW로, 외부 binding은 test Worker와 bindingOverrides로 대체할 수 있어요. 다만 실제 remote binding이나 계정 권한을 확인하는 테스트와는 범위가 다릅니다.
실패한 Worker 로그는 어떻게 확인하나요?
실패 상태에서 server.debug()를 호출하면 server timeline과 캡처된 Worker runtime 로그를 출력할 수 있습니다. server.getLogs()로 구조화 로그를 가져오거나 clearLogs()로 구간을 분리할 수도 있으며, reset() 전에 필요한 로그를 남겨야 합니다.
CLOUDFLARE_ENV는 언제 적용되나요?
Cloudflare Vite plugin의 CLOUDFLARE_ENV는 dev/build 시점에 적용되어 생성 wrangler.json에 반영됩니다. harness나 preview 단계에서 뒤늦게 지정하는 값이 아니므로, production 환경을 검사하려면 build step에서 환경을 선택해야 합니다.