LogoSkills

feature-flag-scenarios

기능 플래그로 진입점을 끌 때 그 위젯을 단언하는 BDD 시나리오를 함께 처리하는 절차 — 기능 전체 off 면 태그 skip(사양 보존), 게이팅 조건만 바뀌면 단언 갱신(should not be displayed), Gherkin 은 Dart 상수를 못 읽으므로 플래그 dartdoc 에 재개 조건 명시, CI 가드와 Key 없는 위젯의 사각지대. ...

기능 플래그 × BDD 시나리오#

한마디로#

"이 버튼 잠깐 감추자"고 플래그를 켜는 PR 과, "이 버튼을 눌러 본다"는 테스트를 추가하는 PR 이 서로 다른 파일이라 git 은 깨끗하게 합칩니다. 각자의 CI 도 자기 브랜치만 보니 둘 다 초록불입니다. 그런데 합쳐진 순간 없는 버튼을 찾는 테스트가 되어 통합 브랜치가 빨간불이 됩니다. 이 스킬은 플래그를 켤 때 테스트를 함께 처리하는 절차입니다.

무엇을·언제#

  • 무엇을 해 주나요: 무엇이 꺼졌는지에 따라 테스트를 보존할지(skip) 계약을 갱신할지를 판정하는 기준을 주고, 사람이 빠뜨려도 CI 가 잡게 합니다.
  • 언제 쓰이나요: 기능 플래그로 버튼·메뉴를 숨길 때, 그리고 나중에 다시 켤 때.
  • 좋은 점: 병렬로 진행되는 두 PR 이 각자 초록불인데 합치면 빨간불이 되는 유형은 리뷰로 잡히지 않습니다. 절차와 가드가 유일한 방어입니다.

핵심 용어#

용어쉬운 설명
기능 플래그코드 배포는 그대로 두고 기능만 껐다 켜는 스위치
게이팅조건에 따라 위젯을 그리거나 안 그리는 것
BDD 시나리오사람이 읽는 문장으로 쓴 테스트
skip테스트를 지우지 않고 잠시 실행에서 빼는 것
Key테스트가 위젯을 찾는 이름표

왜 이 문서가 생겼나#

한 PR 이 플래그를 켜서 버튼을 전 환경 미노출로 바꿨고, 같은 시기 다른 PR 이 그 버튼을 단언하는 BDD 시나리오를 추가했습니다.

# 시나리오를 추가한 PR
When I tap the { ' purchase_refund_button ' } widget at index { ' 0 ' }   # ← 이 Key
// 플래그를 켠 PR
final showRefundAction = !TemporarilyDisabledFeatures.selfRefund &&;  // ← 안 그려진다

git 은 서로 다른 파일이라 깨끗이 머지했고, 각 PR 의 CI 는 자기 브랜치 기준이라 둘 다 green 이었습니다. 충돌은 머지 결과에서만 드러났습니다.

⚠️ 이 규약만으로는 사람이 체크리스트를 지켰을 때만 유효합니다. 병렬 머지가 무검출로 통과하는 구조적 원인(통합 브랜치 보호·머지 큐 부재)은 별건입니다 — 둘이 함께 있어야 재발이 막힙니다.

두 가지 처리 — 무엇이 꺼졌는가로 갈린다#

상황처리근거
기능 전체가 off .feature 태그를 패키지 dart_test.yaml 에서 skip 시나리오는 "재개하면 성립할 미래 사양" — 삭제하면 재개 시 커버리지를 처음부터 다시 써야 한다
게이팅 조건만 변경 (예: 프로덕션 한정 → 전 환경) 시나리오 단언을 갱신 (should not be displayed) 기능은 살아 있고 계약이 달라진 것 — skip 하면 현재 계약이 무검증으로 남는다

skip 하는 경우#

# feature/ < pkg > /dart_test.yaml
tags:
  refund:
    description:  " self-service 환불 시나리오 (purchase_history.feature) " 
     # 재개 시 플래그를 false 로 되돌리고 **이 skip 항목도 함께 제거**한다.
    skip:  " 환불 기능 일시 미노출 — 재개 시 이 항목 제거 "

⚠️ Gherkin 은 Dart 상수를 읽을 수 없습니다. 플래그 하나를 false 로 되돌리는 것만으로는 시나리오가 살아나지 않습니다 — 플래그 dartdoc 에 "재개 시 이 skip 도 제거"를 적어 두세요.

단언을 갱신하는 경우#

Scenario: 프로덕션이 아니어도 공유 생성 진입 버튼은 숨겨진다
  Given the share list is loaded with no shares
  Then the { ' annotation_share_create_entry ' } widget should not be displayed

CI 가드#

플래그가 true 일 때 그 플래그가 게이팅하는 Key 를 긍정적으로 단언하는 시나리오가 skip 없이 살아 있으면 실패시킵니다.

python3 .github/scripts/check_feature_flag_scenarios.py            # 위반 시 exit 1
python3 .github/scripts/check_feature_flag_scenarios.py --report   # 추출 현황
python3 .github/scripts/test_check_feature_flag_scenarios.py       # 가드 자체의 회귀 테스트
시나리오판정
the {'k'} widget should not be displayed✅ 계약을 갱신한 것
the {'k'} widget should be displayed · I tap the {'k'} … ❌ 렌더되지 않는 위젯을 찾는다
위 ❌ 이지만 태그가 dart_test.yaml 에서 skip✅ 사양으로 보존

⚠️ --report 를 함께 보라 — 0건이 "안전"이 아닐 수 있다#

게이팅 추출은 정적 근사입니다. Key 가 없으면 대조할 것이 없어 0건이 나오는데, 그건 안전이 아니라 가드의 사각지대라는 뜻입니다.

🔒 숨김  cart: (게이팅된 Key 없음)         ← 위젯 전체를 early-return 으로 숨김
🔒 숨김  noteTaking: (게이팅된 Key 없음)   ← 그 버튼에 Key 가 없다

find.byIcon·텍스트로 찾는 시나리오는 Key 기반 가드가 닿지 않습니다. 그 계열을 게이팅할 때는 사람이 직접 전수 검색하세요.

rg -n  " 노트|제보|장바구니 "   --glob  ' *.feature '

플래그를 켤 때 체크리스트#

  • 숨기는 위젯의 Key 를 .feature 전수 검색 (rg "\{'그_key'\}" --glob '*.feature')
  • Key 가 없는 위젯이면 아이콘·텍스트 기준으로도 검색
  • 발견한 시나리오를 위 표대로 skip 또는 단언 갱신
  • skip 했다면 플래그 dartdoc 에 "재개 시 이 skip 도 제거" 를 명시
  • 가드 스크립트 통과 + --report 로 사각지대 확인

플래그를 끌(재개할) 때#

  • dart_test.yaml 의 대응 skip 항목 제거
  • 갱신했던 should not be displayed 단언을 원래 계약으로 되돌림
  • 플래그 dartdoc 의 "재개 조건" 이 실제로 해소됐는지 확인

관련#