개념
테스트 용이성은 요구사항이지 부탁이 아니다
자동화가 깨지는 원인의 상당수는 자동화 코드 밖에 있다. 클래스명이 바뀌는 버튼, 현재 시각에 따라 결과가 달라지는 화면, 초기화할 방법이 없는 상태, 외부 결제사가 멈추면 같이 멈추는 흐름. 이런 조건에서 자동화를 안정화하려는 노력은 대부분 헛수고 다 — 구조로 못 막는 문제를 기법으로 덮는 중이기 때문이다.
전환점은 말의 틀을 바꾸는 데서 온다. "셀렉터 좀 안 바뀌게 해 주실 수 있나요" 는 부탁 이고, "이 화면은 자동 검증이 불가능한 상태이며 그 비용은 회귀 1회당 수동 2시간입니다" 는 요구사항 제기 다. 레슨 1이 쓴 gTAA 어휘로 말하면 테스트 용이성은 SUT 가 적응 계층에 제공해야 하는 인터페이스 품질 이고, ISTQB 실러버스가 자동화 인프라와 함께 테스트 용이성을 다루는 이유도 그것이 TAS 의 전제 조건이기 때문이다.
요구 항목은 두 축으로 정리하면 빠진 것이 보인다.
- 관측성(observability) — 시스템의 상태를 밖에서 읽을 수 있는가. 안정 식별자, 구조화 로그, 상관 ID 전파, 상태 조회·버전 엔드포인트.
- 제어성(controllability) — 시스템을 원하는 상태로 만들 수 있는가. 시드 데이터 주입, 상태 초기화, 시간 고정, 외부 연동 스텁, 기능 플래그 강제.
자동화가 어려운 화면은 거의 항상 둘 중 하나가 없다. 어느 쪽이 없는지 먼저 말하면 대화가 요구 목록으로 바로 넘어간다.
요구 목록 1 — 안정 식별자 (가장 싸고 가장 효과가 크다)
<!-- ✗ 자동화가 깨지는 구조: 스타일이 식별자를 겸한다 -->
<button class="btn btn-primary mt-2">주문 취소</button>
<!-- ○ 테스트 전용 식별자. 리팩터링·디자인 변경과 분리된다 -->
<button class="btn btn-primary mt-2" data-testid="order-cancel">주문 취소</button>요구할 것은 속성 하나가 아니라 규칙 이다. (1) 속성 이름을 하나로 통일하고
(data-testid 가 Playwright 기본값이며 testIdAttribute 로 바꿀 수 있다), (2) 값의
작명 규칙을 정하고(영역-대상-동작 처럼), (3) 그 속성을 지우는 변경은 리뷰에서
막는다. 셋 중 셋째가 빠지면 반년 뒤 절반이 사라져 있다.
개발 비용은 거의 없다. 그래서 이것부터 요구한다 — 첫 요구가 거절당하면 다음 요구는
훨씬 어려워진다. 다만 접근성 속성이 이미 잘 붙은 제품이라면 역할·이름 기반 로케이터
(getByRole)가 먼저이고, 그것으로 안 되는 지점에만 테스트 id 를 요구하는 쪽이 설득력이
높다(모듈 13 레슨 3).
요구 목록 2 — 테스트 모드와 훅
| 요구 | 없을 때 자동화가 치르는 비용 |
|---|---|
| 시간 고정·이동(쿠폰 만료·정산 마감 등) | 해당 시나리오를 아예 자동화 못 하거나 날짜마다 깨진다 |
| 외부 연동 스텁(PG·문자·배송) | 외부 장애가 곧 테스트 실패. 결제 테스트 비용 발생 |
| 기능 플래그 강제 지정 | 플래그 상태에 따라 간헐 실패 |
| OTP·캡차 우회(테스트 환경 한정) | 로그인 자동화가 불가능 |
여기서 반드시 함께 설계할 것이 프로덕션 유출 방지 다. 테스트 훅은 그 자체로 취약점이 될 수 있으므로, 요구할 때 가드를 함께 제안 해야 개발팀이 받아들인다.
// 서버 측 예시 — 테스트 전용 라우트는 세 겹으로 막는다
export function mountTestHooks(app: Express) {
if (process.env.NODE_ENV === 'production' || process.env.TEST_HOOKS !== 'on') {
return; // 1) 환경에서 아예 마운트되지 않는다
}
app.use('/test', requireSharedSecret()); // 2) 전용 토큰 없이는 호출 불가
app.post('/test/clock', setFrozenTime); // 3) 감사 로그에 호출을 남긴다
app.post('/test/seed/orders', seedOrders);
}제안의 요지는 "테스트를 위해 구멍을 내 달라" 가 아니라 "환경 변수로 꺼지고 토큰으로 잠기며 호출이 로그에 남는 전용 경로를 하나 두자" 다. 이 문장을 준비해 가면 보안 담당 자가 반대하기 어렵다. 반대로 가드 없이 요구하면 한 번 거절당하고 그 뒤로는 주제 자체가 금기가 된다.
요구 목록 3 — 상태 초기화와 시드 API
레슨 2에서 전제 조건은 API 로 만든다고 정했다. 그 API 가 없으면 UI 경유 시딩으로 떨어지고, 테스트 하나당 수십 초가 더 붙는다.
POST /test/seed/orders { "status": "paid", "sku": "SKU-1001" } → { "orderNo": "..." }
DELETE /test/orders/{no} → 204
POST /test/reset { "scope": "tenant", "tenantId": "qa-7" } → 204요구를 통과시키는 핵심은 범위를 작게 쪼개 들어가는 것 이다. "테스트용 API 를 만들어
주세요" 는 막연해서 백로그 맨 뒤로 간다. "회귀 자동화 12건이 막혀 있고, 그 12건은
결제완료 주문 1건 생성 하나만 있으면 풀립니다" 는 스토리 하나로 들어간다.
초기화의 대안도 함께 제시한다 — 전체 리셋이 위험하다면 테넌트·계정 단위 격리 로 좁히고, 그것도 어렵다면 테스트가 만든 데이터만 역추적해 지우는 쪽으로 간다. 선택지를 두 개 이상 들고 가면 거절이 아니라 선택이 된다.
요구 목록 4 — 관측 가능성
레슨 3의 상관 ID 구조는 서버가 그 헤더를 로그에 남겨 줄 때만 작동한다. 그래서 이것은 자동화 쪽 요구가 아니라 공통 로깅 규약 요구다.
- 들어온
x-correlation-id를 받아 그대로 로그 필드에 기록하고, 하위 호출에 전파한다 - 요청 로그에 상태 코드·소요 시간·사용자 식별자를 구조화 필드 로 남긴다
- 배포 버전·커밋 해시를 알려주는 엔드포인트를 둔다 — 실패가 어느 빌드 에서 났는지 리포트에 적을 수 있다
- 비동기 작업(배치·큐)의 완료를 조회할 방법을 둔다 — 없으면 테스트가 대기로 때운다
마지막 항목이 특히 값지다. 큐 처리 완료를 폴링할 상태 조회가 없으면 자동화는
waitForTimeout 으로 때우게 되고, 그 지점이 플래키의 주요 생산지가 된다. 즉
관측성 결핍은 플래키로 환전된다.
언제 요구하는가 — 설계 단계에 끼어든다
구현이 끝난 뒤에 하는 요구는 재작업 요청이 되어 거의 거절된다. 끼어들 지점은 셋이다.
| 시점 | 할 일 | 형식 |
|---|---|---|
| 리파인먼트 | 테스트 용이성 질문 3개를 던진다 | "이 상태는 어떻게 만들 수 있나요?" |
| 설계 리뷰 | 식별자·훅·로그 요구를 설계 항목으로 올린다 | 리뷰 코멘트 |
| PR 리뷰 | 합의한 항목이 실제로 들어왔는지 본다 | 체크리스트 |
리파인먼트에서 쓰는 질문 세 개는 외워 둔다 — "이 화면의 성공·실패 상태를 어떻게 만들죠?", "이게 잘 동작했는지 UI 말고 무엇으로 확인하죠?", "외부 연동은 테스트 환경에서 무엇으로 대체되죠?" 이 질문은 요구처럼 들리지 않으면서 요구와 같은 일을 한다. 그리고 세 질문에 답이 없다는 사실 자체가 설계가 덜 됐다는 신호 라서, 개발팀도 답하면서 얻는 게 있다. 요구를 개발팀의 이익으로 번역하는 것이 협상의 기본이다 — 테스트 모드와 시드 API 는 개발자의 로컬 재현 시간도 줄인다.
거절당했을 때 — 대안과 비용을 같이 내민다
거절은 정상이다. 준비해야 할 것은 설득이 아니라 대안과 그 비용 이다.
| 거절된 요구 | 차선 | 비용 |
|---|---|---|
| 안정 식별자 | 역할·텍스트 기반 로케이터 | 문구 변경마다 수정. 다국어면 더 심함 |
| 시드 API | UI 경유 시딩 | 테스트당 20~60초 추가, 실패 지점 증가 |
| 시간 고정 | 상대 시간으로 케이스 재설계, 일부 포기 | 만료·마감 시나리오 커버리지 손실 |
| 외부 연동 스텁 | 샌드박스 계정 사용, 야간 실행 | 외부 장애가 실패로 들어옴 |
| 상태 초기화 | 실행마다 신규 계정 생성 | 데이터 누적, 정리 잡 필요 |
이 표를 들고 가는 목적은 압박이 아니다. 비용을 누가 지불할지 결정하게 만드는 것 이다. 차선으로 가기로 했다면 그 비용(추가 실행 시간, 커버리지 공백, 플래키 증가)을 기록에 남긴다 — 리스크 등록부나 자동화 백로그에 한 줄로. 6개월 뒤 "자동화가 왜 느리냐" 는 질문이 올 때 이 기록이 답이 된다. 반대로 기록하지 않으면 그 비용은 QA 의 역량 문제로 해석된다.
그리고 우리가 PR 을 보내는 선택지 를 늘 가지고 있는다. 안정 식별자 추가는 대개 QA 가 직접 보낼 수 있는 변경이다. "해 주세요" 보다 "이렇게 올려 뒀으니 봐 주세요" 가 통과율이 훨씬 높고, 한 번 통과하면 그 뒤로는 관행이 된다.
DoD·수용 기준에 넣어 영구화한다
한 번의 협상으로 얻은 것은 다음 스프린트에 사라진다. 제도화 지점은 두 곳이다.
완료 정의(모듈 48 레슨 2)에 넣는 문구 예시 — 팀 전체에 한 번 적용된다.
- 새로 추가·변경된 상호작용 요소에 합의된 테스트 식별자가 있다
- 새 기능의 주요 상태를 만들 방법(시드 또는 훅)이 문서화돼 있다
- 서버 로그에 상관 ID 가 전파되고, 신규 엔드포인트도 이를 따른다
수용 기준 에 넣는 문구 예시 — 스토리 하나에 붙는다.
- (주어진) 쿠폰 만료 시나리오를 검증하려면 (그때) 테스트 환경에서 기준 시각을 지정할 수 있어야 한다
- 주문 상태 조회 응답에 상태 전이 시각이 포함되어, UI 없이도 결과를 확인할 수 있다
문구를 쓸 때의 원칙은 검증 가능하게 쓰는 것 이다. "테스트하기 쉽게 만든다" 는 판정할 수 없어서 DoD 에 들어가도 아무 일도 일어나지 않는다.
실무에서 왜 필요한가
첫째, 자동화의 상한은 테스트 용이성이 정한다. 아무리 좋은 프레임워크도 상태를 만들 수 없는 시나리오는 자동화하지 못한다. 자동화 커버리지가 60%에서 멈추는 팀의 원인은 대개 기술이 아니라 제어성 결핍이다.
둘째, 협상 능력이 자동화 엔지니어와 자동화 리드를 가른다. 코드로 풀 수 없는 문제를 알아보고, 그것을 개발팀의 언어와 비용으로 번역해 설계 단계에 밀어 넣는 일은 직접 구현보다 레버리지가 크다. 식별자 규칙을 DoD 에 한 줄 넣는 것이 셀렉터 수정 300건을 없앤다.
셋째, 거절을 기록으로 남기는 습관이 QA 의 신뢰를 지킨다. 테스트 용이성을 요구했고 거절됐으며 그 대가가 무엇인지 적혀 있다면, 나중에 발생한 공백은 결정의 결과다. 적어 두지 않으면 같은 공백이 "자동화가 부실하다" 로 돌아온다.