Cucumber 시나리오의 절반이 skipped됩니다. 실패한 것이 아닙니다. skipped입니다.

그 노란색 상태는 빨간색 빌드보다 더 나쁩니다. 정중한 라벨 하나 아래에 서로 완전히 다른 세 가지 문제를 숨기면서 스위트를 건강해 보이게 만듭니다. skipped 시나리오는 태그 필터가 제외했거나, step definition이 사라졌거나, 첫 번째 스텝이 실행되기도 전에 Before hook에서 예외가 발생했음을 의미할 수 있습니다. 당신의 임무는 어떤 기생충이 스위트 안에 살고 있는지 밝혀내는 것입니다.

”Skipped”가 Cucumber에서 가장 위험한 상태인 이유

Cucumber는 시나리오에 대해 passed, failed, skipped라는 세 가지 종료 상태를 가집니다. passed와 failed는 정직합니다. skipped는 쓰레기통입니다.

시나리오가 skipped되면 Cucumber는 어떤 전제조건이 충족되지 않았음을 알려주는 것입니다. 문제는 그 “전제조건”이 의도적인 태그 필터에서부터 설정 hook의 null pointer까지 무엇이든 될 수 있다는 것입니다. 시나리오의 50%가 skipped된 스위트는 500개의 테스트를 실행한 것처럼 보입니다. 그렇지 않습니다. 250개의 테스트를 실행하고 나머지 250개를 검증하지 않은 채 손을 흔든 것뿐입니다.

대부분의 CI 대시보드는 skipped를 중립적으로 취급합니다. 파이프라인은 녹색입니다. 하지만 행동 명세의 절반은 검증되지 않고 있습니다. Cucumber를 살아있는 문서(living documentation)로 사용하고 있다면, 문서의 절반은 거짓말입니다.

대량 Skipping의 세 가지 근본 원인

Cucumber에서 skipped 시나리오를 만드는 방법은 정확히 세 가지입니다. 그중 두 가지는 버그입니다. 하나는 마치 기능처럼 보이는 잘못된 설정입니다.

의도보다 더 많은 것을 제외하는 태그 필터

시나리오 50%가 skipped되는 가장 흔한 원인은 runner 설정의 태그 표현식이 생각보다 넓게 설정되어 있기 때문입니다. CI 프로필은 다음과 같을 수 있습니다:

// cucumber.js
module.exports = {
  default: [
    '--format', 'progress',
    '--tags', 'not @wip',
  ].join(' '),

  ci: [
    '--format', 'json:reports/cucumber.json',
    '--tags', 'not @wip and not @slow',
  ].join(' '),
};

진짜 피해는 팀이 긍정적 태그 필터(positive tag filter)를 사용할 때 발생합니다. --tags '@regression' 같은 필터는 @regression 태그가 붙은 시나리오만 실행합니다. 나머지 모든 시나리오는 skipped됩니다. 단 하나의 feature 파일도 바꾸지 않고 100% 실행에서 50% 실행으로 떨어지는 것이 바로 이런 방식입니다.

더 이상 매칭되지 않는 Step Definition

어떤 step이라도 매칭되는 step definition이 없으면 Cucumber는 전체 시나리오를 skip합니다. 이것은 실패가 아닙니다. skip입니다. CI는 여전히 녹색입니다. 생각보다 자주 발생합니다. 제품 관리자가 step의 오타를 고치기 위해 Gherkin 파일을 수정합니다:

# features/checkout.feature
Feature: Checkout

  Scenario: Guest user completes purchase
    Given a guest user with items in cart
    When they proceed to checkout
    Then the order total should include tax

당신의 step definition은 이렇게 기대합니다:

// features/step_definitions/checkout_steps.js
Given('a guest user with items in the cart', function () {
  // setup
});

feature 파일에 “the”가 빠져 있는 것을 주목하세요. Cucumber의 regex는 더 이상 매칭되지 않습니다. 전체 시나리오가 skip됩니다. 전체 스위트를 로컬에서 실행하지 않고 feature 파일을 리팩토링하는 팀 전체에 이것을 곱하면, 커버리지의 절반을 drift로 잃을 수 있습니다.

스스로를 Skip으로 위장하는 실패하는 Hook

Before hook에서 예외가 발생하면 Cucumber는 해당 feature 파일의 모든 시나리오를 skip합니다. 실패시키지 않습니다. skip합니다.

// features/support/hooks.js
const { Before } = require('@cucumber/cucumber');

Before(async function () {
  this.browser = await chromium.launch();
  this.page = await this.browser.newPage();
  // If this throws, every scenario in the file is skipped
  await this.page.goto(process.env.TEST_URL);
});

CI에서 TEST_URL이 정의되지 않으면 hook은 예외를 던지고 이에 연결된 모든 시나리오가 skip됩니다. 빌드는 녹색입니다. 테스트는 쓸모없습니다.

어떤 기생충이 있는지 진단하는 방법

세 가지 원인 중 어떤 것이 책임이 있는지 모른 채 50% skipped 시나리오를 고칠 수는 없습니다. Cucumber의 기본 출력은 알려주지 않습니다. 직접 캐물어야 합니다.

태그 필터 없이 --dry-run으로 스위트를 실행하세요:

npx cucumber-js --dry-run --tags ''

dry run은 어떤 것도 실행하지 않고 모든 feature 파일을 파싱하고 각 step을 step definition과 매칭합니다. 100% undefined가 나오면 step definition drift가 있는 것입니다. 100% passed가 나오면 skip은 태그 필터나 runtime hook에서 오는 것입니다.

JSON 포맷터는 각 step에 대해 status 필드를 포함합니다. 태그 필터로 skip된 시나리오에는 step이 전혀 없습니다. step definition이 없어서 skip된 시나리오는 매칭되지 않는 step에 undefined가 표시됩니다. 실패하는 hook으로 skip된 시나리오는 모든 step에 skipped가 표시되고, embeddings 배열에 hook 오류가 포함될 수 있습니다.

npx cucumber-js --format json:report.json

JSON을 파싱하고 step이 전혀 없는 시나리오와 undefined 또는 skipped 상태의 step을 가진 시나리오가 몇 개인지 세세요. 그것이 어디를 봐야 하는지 알려줍니다.

가장 안전한 장기적인 해결책은 예상치 못한 skip을 빌드 실패로 처리하는 것입니다. JSON 리포트용 작은 후처리기가 이 일을 합니다:

// scripts/fail-on-skips.js
const fs = require('fs');

const report = JSON.parse(fs.readFileSync('report.json', 'utf8'));

let unexpectedSkips = 0;

for (const feature of report) {
  for (const element of feature.elements) {
    if (element.type !== 'scenario') continue;

    const hasSkip = element.steps.some(s => s.result?.status === 'skipped');
    const hasUndefined = element.steps.some(s => s.result?.status === 'undefined');

    // Allow @wip to skip; everything else must run
    const isWip = element.tags?.some(t => t.name === '@wip');

    if ((hasSkip || hasUndefined) && !isWip) {
      console.error(`Unexpected skip: ${feature.name} > ${element.name}`);
      unexpectedSkips++;
    }
  }
}

if (unexpectedSkips > 0) {
  console.error(`\n${unexpectedSkips} scenario(s) skipped unexpectedly. Failing build.`);
  process.exit(1);
}

CI에서 매 Cucumber 실행 후 이것을 실행하세요. 이것은 태그 설정 오류, 누락된 step definition, 실패하는 hook이 스위트를 조용히 썩기 전에 잡아낼 것입니다.

트레이드오프: 왜 일부 Skipping은 괜찮은가

의도적인 skipping은 유효한 도구입니다. @wip 태그는 이유가 있어서 존재합니다. 구현 전에 시나리오를 작성하고 @wip로 표시한 다음, 코드가 준비될 때까지 runner가 이를 skip하게 합니다.

건강한 skipping과 스위트 부패 사이의 차이는 위생입니다. @wip는 일시적이어야 합니다. 브랜치에 있어야지, 6개월 동안 main에 있어서는 안 됩니다. 기본 브랜치에서 시나리오의 50%가 @wip 태그가 붙어 있다면, 테스트 스위트가 아닙니다. 희망사항 목록입니다.

태그 기반 필터링은 환경별 테스트에도 의미가 있습니다. 실제 결제 단말기가 필요한 시나리오는 CI에서 실행되어서는 안 됩니다. 하지만 그 시나리오는 @hardware로 태그되어야 하고, @slow@manual이 아니어야 합니다. 무언가가 제외되는 이유를 명시적으로 하고, 코드를 리뷰하는 것과 동일한 방식으로 코드 리뷰에서 그러한 제외 사항을 감사하세요.

부패를 멈추는 방법

오늘 50% skipped 상태라면, 정직함으로 돌아가는 가장 빠른 길은 다음과 같습니다.

  1. 태그 없이 dry run을 실행하세요. undefined step을 세세요. 모든 불일치를 고치세요. 이것은 보통 5분짜리 찾아바꾸기 작업입니다.

  2. runner 프로필을 감사하세요. cucumber.js, Maven pom.xml, 또는 Gradle 설정의 모든 태그 표현식을 나열하세요. 각각이 의도적인지 확인하세요. 긍정적 필터(--tags '@regression')를 부정적 필터(--tags 'not @wip')로 바꿔서 새 시나리오가 기본적으로 실행되게 하세요.

  3. skip-check 스크립트를 CI에 추가하세요. 예상치 못한 skip이 발생하면 빌드를 실패시키세요. 이것은 한 번 설정하면 영원히 이익을 주는 것입니다.

  4. 월간 태그 감사를 예약하세요. feature 파일에서 @wip를 검색하고 개수를 세세요. 숫자가 늘어나고 있다면, 도구 문제가 아니라 프로세스 문제입니다.


FAQ

왜 Cucumber는 step definition이 없을 때 시나리오를 실패시키지 않고 skip하나요? Cucumber는 누락된 step definition을 코드 결함이 아닌 불완전한 명세로 취급합니다. 시나리오는 Cucumber가 이해하지 못하는 것을 실행할 수 없기 때문에 skip됩니다. 이것은 원래 Ruby 구현의 역사적인 동작이며, 포트 전반에 걸쳐 유지됩니다. 실패하게 만드는 유일한 방법은 undefined 상태를 확인하는 후처리기를 추가하는 것입니다.

skipped 시나리오와 pending 시나리오의 차이는 무엇인가요? 현대 Cucumber 버전에서 “pending”은 step definition에 의해 명시적으로 던져지는 step 수준의 상태입니다(예: Ruby에서 pending()을 호출하거나 JavaScript에서 'pending'을 반환하는 것). “Skipped”는 전제조건이 실패하거나, 태그 필터가 시나리오를 제외하거나, 이전 step이 실패했을 때 적용되는 시나리오 수준의 상태입니다. 실제로 둘 다 노란색으로 표시되고 둘 다 “이것은 완료까지 실행되지 않았음”을 의미합니다.

누락된 step definition에서 Cucumber를 실패하게 만들 수 있나요? 대부분의 Cucumber 포트에는 이를 위한 내장 CLI 플래그가 없습니다. JSON이나 JUnit 출력을 파싱해서 직접 빌드를 실패시켜야 합니다. 이 글의 스크립트는 최소한의 작동 예제입니다.

스위트에서 실제로 사용 중인 태그를 어떻게 찾나요? feature 파일 전체에서 grep을 실행하세요: grep -roh '@[a-zA-Z0-9_-]*' features/ | sort | uniq -c | sort -rn. 이것은 모든 태그의 빈도표를 제공합니다. 이를 runner 프로필과 교차 참조하여 문서화 없이 필터링하는 태그를 찾으세요.