서론
안녕하세요. 비즈니스 영상 미디어 'PIVOT'에서 QA 엔지니어 겸 PjM을 맡고 있는 카이스케입니다.
PIVOT의 모바일 앱(iOS / Android)은 AI를 이용해 테스트 케이스를 작성하게 되면서, 케이스 수가 계속해서 늘어나고 있습니다. 출시 전 UAT(사용자 인수 테스트)에서 모든 케이스를 실행하는 것이 어려워졌기 때문에, 리스크 기반 테스트(RBT)를 통해 실행할 케이스를 선택하기로 했습니다. 규칙은 한 번에 정해지지 않아 여러 번 수정하고 있습니다. 이 글은 그 과정에서 시도했던 것들과 알아낸 점들을 기록한 것입니다.
전제
테스트 케이스를 명세서(Specification)로 활용
PIVOT에서는 테스트 케이스를 명세서의 역할로 사용하고 있습니다. 기능 명세서를 별도로 만들지 않고, 테스트 케이스를 '현재 앱이 어떻게 작동하는가'에 대한 원본 자료로 삼고 있습니다.
테스트 관련 산출물은 다음 3가지로 나누어 보관하고 있습니다.
| 산출물 | 보관 위치 | 내용 |
|---|
| 테스트 관점 | GitHub 리포지토리 | 기능별로 무엇을 확인할 것인지 목록 |
| ... | | |
| 정의는 기능을 변경할 때마다 해당 PR(Pull Request)에서 줄을 추가하거나, 수정하거나, 삭제합니다. PR 리뷰를 거치기 때문에 정의만 읽으면 현재 동작 방식을 알 수 있습니다. 실행 결과는 정의에 적지 않고, 출시별 실행 기록에 작성합니다. | | |
명세서 역할을 겸하고 있기 때문에 케이스가 세분화됩니다. 비회원일 때, 로그아웃 후, iPad를 사용할 때, 뒤로 가기 조작을 했을 때, 다른 화면에서 진입했을 때 등 조건의 차이를 하나하나 케이스로 만듭니다.
이러한 방식으로 분리했기 때문에 나중에 RBT를 도입할 때도 정의는 한 줄도 지우지 않고 유지할 수 있었습니다. 실행할 케이스를 선택하는 것은 오직 실행 기록 상에서만 하기 때문입니다.
과제
AI로 작성하니 그 속도로 증가함
테스트 케이스는 PRD(제품 요구사항 정의서)나 Figma, PR의 차이점 분석을 통해 AI가 작성하고 있습니다. 세부적인 케이스라도 AI를 이용하면 짧은 시간에 작성할 수 있어, 기능이 만들어질 때마다 케이스 수가 그 속도에 맞춰 증가했습니다. 한 번의 출시에서 UAT 실행 대상이 277건에 달했던 적도 있습니다.
줄일 때 기준이 남지 않음
그전까지는 이번 출시에서 동작한 기능의 케이스를 전량으로 실행하고 있었습니다. 케이스가 명세서로서 세분화될수록 UAT 건수도 그만큼 늘어납니다. 건수를 보고 케이스를 줄이는 경우도 있었지만, 어떤 기준으로 줄였는지 기록에 남지 않았습니다. 무엇을 실행하지 않았는지는 실행 기록을 보면 알 수 있습니다. 하지만 왜 그것을 실행할 필요가 없다고 판단했는지에 대한 근거는 다음 출시에서 같은 판단을 할 때 참조할 수 없었습니다.
케이스는 명세서이기 때문에 줄이고 싶지 않습니다. 그래서 케이스는 그대로 남겨두고, UAT에서 무엇을 실행할지만 매번 설명 가능한 형태로 선택하기로 했습니다.
시도한 것
먼저 기능 기반으로 RBT를 시도해 봄
RBT의 기본 원칙인 '영향도 × 발생 확률'로 리스크를 보는 방식은 처음부터 지금까지 변하지 않았습니다. 바꾼 것은 영향도를 부여하는 단위와 케이스의 배분입니다. 처음에는 영향도를 기능별로 부여했습니다. 재생이나 로그인은 높게, 팔로우나 마일 표시 등은 낮게 설정하는 식입니다. 발생 확률은 출시마다, 해당 기능의 동작을 변경했다면 3, 내부 대체만 했다면 2, 건드리지 않았지만 파급될 수 있다면 1로 했습니다.
기능 내에서는 케이스를 '무엇의 차이를 확인하고 있는지'에 따라 정상계/상태/조작/화면/이상 5가지로 나누고, 리스크가 높은 기능일수록 많은 종류를 실행하도록 했습니다. UAT에서 발생하는 결함은 정상계보다는 조건의 차이에서 나오는 경우가 많았기 때문입니다.
이 방식으로 지난 5회 출시의 케이스를 재선정하여 당시 UAT에서 발견했던 결함을 잡아낼 수 있었는지 확인했습니다. 실행하는 테스트 케이스 수는 후보 전체 케이스 대비 83%(980건 중 809건)였으며, 결함 13건 중 11건을 포착할 수 있었습니다. 조건의 차이를 세밀하게 잡도록 선택하면 5회 모두 누락이 0에 가까워지지만, 실행하는 테스트 케이스 수는 후보 전체 대비 92%로 거의 줄어들지 않았습니다. 잡아내지 못한 2건은 조건이 한정적이어서 사용자가 직접 회피할 수 있는 결함이었습니다. 이에 따라, 실행하는 테스트 케이스 수를 후보 전체의 약 80% 수준으로 유지하고 나머지는 리스크로 받아들이기로 판단했습니다.
리팩토링(Refactor) 출시에서 기능 기반의 한계가 드러남
곤란했던 것은 iOS 앱의 멀티모듈화가 적용된 릴리스였습니다. 거의 모든 화면의 디렉토리 구조가 바뀌었기 때문에, 거의 모든 기능의 발생 확률이 2로 높아졌습니다. 기능 기반 대장(ledger)에서는 영향도가 높은 기능의 리스크가 올라가면 해당 기능의 케이스들이 한데 모여 실행되는 쪽으로 들어갑니다. 만약 기능 A에 대한 테스트 관점이 1부터 10까지 있다면, 1부터 10까지 전부를 돌리게 됩니다. 그 결과, 후보 건수는 1,339건이 되었습니다.
하나의 기능 안에도, 충족되지 않으면 영상을 볼 수 없게 되는 확인과 단순히 표시만 하는 확인이 섞여 있습니다. 영향도를 '기능' 단위로 부여하는 한, 이 두 가지를 분리할 수 없습니다. 이 경우에는 사람이 약 5회에 걸쳐 범위를 좁혀 516건으로 줄였습니다.
영향도를 유저 스토리 × 수용 기준(Acceptance Criteria)으로 재설정하다
이에 따라 케이스들을 '유저 스토리와 수용 기준'에 맞춰 다시 배치하고, 수용 기준별로 영향을 부여한 대장을 만들었습니다. 영향도는 해당 기준이 충족되지 않았을 때 사용자가 겪게 되는 상황에 따라 4단계로 나눴습니다.
| 단계 | 발생할 일 |
|---|
| S1 보이지 않음 | 영상을 볼 목적을 달성하지 못하고, 대체 경로도 없음 |
| ... | |
| 예를 들어 재생 스토리에서는 '재생 버튼을 누르면 재생이 시작된다'는 충족되지 않으면 영상 시청이 불가능하므로 S1입니다. 반면 '재생 속도를 변경할 수 있다'는 충족되지 않아도 일반 속도로 볼 수 있으므로 S3입니다. | |
테스트 관점을 단위로 삼지 않은 이유는, 관점 자체가 릴리스마다 PR(Pull Request) 단위로 쌓여왔고, 무게가 다른 확인 사항들이 하나의 관점에 혼재되거나, 다른 관점이 같은 것을 보고 있는 경우가 많았기 때문입니다. 수용 기준이라면, 하나의 기준이 오직 한 가지 동작만을 보도록 작성할 수 있습니다.
발생 확률 역시 수용 기준별로 부여하도록 했습니다. 동일 기능 내에서도 이번 릴리스의 PR에서 동작이 변경되는 수용 기준은 3으로 설정합니다. 동작이 변하지 않은 수용 기준, 즉 코드 위치만 이동했거나 다른 변경 사항이 파급될 가능성이 있는 것은 2 또는 1로 합니다.
초기 배정이 느슨하여 건수가 줄지 않았다
수용 기준 단위로 재배치한 후 멀티모듈화 릴리스에 적용했을 때는 1,046건이었습니다. 기능 기반의 1,339건에서 크게 줄어들지 않았습니다.
원인은 단위 자체가 아니라, 발생 확률이 2 또는 1인 수용 기준에 대해 얼마나 많은 케이스를 돌릴지 결정하는 방식에 있었습니다. 처음에는 S3 수용 기준이라면, 발생 확률이 2나 1이라도 정상 케이스를 각각 1건씩 실행하기로 했습니다. 하지만 멀티모듈화에서는 코드 위치만 이동했을 뿐, 대부분의 수용 기준 동작은 변하지 않았습니다. 따라서 대부분의 수용 기준이 발생 확률 2가 되었고, 그중 상당수가 S3였습니다. 결과적으로 거의 모든 수용 기준에서 최소 1건씩 실행하게 되어, 돌리지 않는 기준이 거의 남지 않았습니다.
단순히 단위를 세분화하는 것만으로는 건수가 줄어들지 않습니다. 어떤 기준을 제외할지 결정해야 비로소 줄어듭니다. 그래서 발생 확률이 2나 1인 S3·S4 수용 기준은, 과거에 결함이 발생했던 조건의 케이스만 실행하도록 했습니다. S3는 케이스로는 돌리지 않지만, Ad-hoc 테스트 시 주의 깊게 확인합니다. 현재 선택 방식은 다음 표와 같습니다.
| 영향도 | 이번 PR에서 동작이 변경되는 기준 (발생 확률 3) | 동작은 변하지 않는 기준 (발생 확률 2・1) |
|---|---|
| S1 보이지 않음 | 전 항목 | 전 항목 |
영향도와 발생 확률을 곱해서 점수를 매기는 단계는 생략했습니다. 표만 봐도 왜 그 케이스를 돌리는지 바로 읽을 수 있는 것이 PM(Product Manager)과 잔존 리스크에 대해 논의할 때 더 편했기 때문입니다.
선정은 Claude Code의 스킬(skill)에게 맡겼다
규칙을 바꿀 때마다 손으로 다시 선택하는 것은 힘들기 때문에, 선정 작업도 AI에 맡기고 있습니다. 릴리스 티켓을 Claude Code의 스킬에 전달하면, 이번 PR들을 모아 기능별로 정리하고, 발생 확률을 근거와 함께 저에게 확인한 후, 스크립트를 통해 케이스를 선택하여 Notion에 실행 기록을 만듭니다. 즉, AI가 늘린 케이스를 AI가 선정하여 돌리는 형태입니다.
규칙의 정본(Source of Truth)은 문서로 두고, 스킬과 내용이 다르면 문서를 기준으로 삼기로 했습니다. 여러 번 다시 만들면 알게 되었기 때문에, 사람이 읽을 수 있는 문서를 먼저 수정할 수 있도록 하고 싶었습니다.
효과
실행하는 테스트 케이스 수가 약 절반으로 줄어들었다
멀티 모듈화 릴리스의 경우, 기능 기반 규칙으로는 후보가 1,339건이었고, 인수 기준 단위로 배분한 첫 번째 할당에서는 1,046건이었습니다. 배분을 재검토한 후의 규칙으로 다시 적용하니 628건이 되었습니다. 이 628건은 S4의 인수 기준에서도 과거에 결함이 발생했던 조건은 실행한다는 현재 표의 처리 방식을 넣기 전의 수치입니다.
실행하지 않은 기준을 설명할 수 있게 됨
어떤 인수 기준을, 어떤 기준으로 실행하지 않았는지 실행 기록에 남게 되었습니다. 영향도와 발생 확률의 조합이 적혀 있기 때문에, 릴리스 후에 결함이 발생했을 때 그 판단이 타당했는지, 또는 대장의 영향도를 수정해야 하는지 검토할 수 있습니다.
확보된 시간을 탐색에 활용할 수 있게 됨
과거 릴리스를 검토해 보면, 케이스에 대응하지 않는 UAT 결함은 원래 탐색적 테스트(Exploratory Testing)에서 발견되고 있었습니다. 케이스를 줄여서 생긴 시간은 영향도가 높은 기능의 탐색과 S3의 인수 기준을 주의 깊게 살펴보는 애드혹 테스트(Ad-hoc Test)에 할애하고 있습니다.
요약
알게 된 점
- 테스트 케이스를 명세서처럼 다루면, 케이스가 세분화됩니다. AI로 작성하면 그 속도대로 수가 증가합니다.
- 영향도를 기능 단위로 부여하면, 리팩토링(Refactoring)처럼 광범위하게 건드리는 릴리스에서 기능별 케이스 전체를 모두 실행해야 합니다.
- 영향도의 단위를 세밀하게 했다고 해서 건수가 줄어들지는 않습니다. 발생 확률이 낮은 기준을 어디까지 실행하지 않을지 결정해야 비로소 줄어듭니다.
- 케이스 선택 방식만으로는 결함을 1건도 놓치지 않으면서 동시에 건수를 크게 줄이는 것은 양립할 수 없습니다. 어느 정도를 리스크로 받아들일지 결정할 필요가 있습니다.
케이스가 과도하게 늘어나 어려움을 겪는 팀에게
- 영향도는 기능이 아닌 인수 기준처럼 '하나의 동작(behavior)'을 보는 단위로 부여합니다.
- 실행할 케이스를 정하는 표에는 영향도와 발생 확률 조합별로 '몇 건을 실행할지'를 작성해 둡니다. 점수화하는 것보다, 왜 실행하는지를 설명하기 쉽습니다.
- 새로운 규칙은 과거 릴리스의 케이스 및 결함에 적용하여, 건수와 발견된 결함을 확인한 후에 사용합니다.
- 실행하지 않은 케이스는 이유와 함께 실행 기록에 남깁니다.
토론 (Discussion)