[SE100 #042] 블랙박스 기법 — 동등 분할과 경계값 분석
소프트웨어 공학 100 주제 시리즈의 42번째 글이다. (카테고리: 소프트웨어 테스팅)
한 줄 요약
동등 분할(EP) 은 “프로그램이 똑같이 처리할 것으로 기대되는” 값들을 한 묶음으로 보고 묶음마다 대표값 하나만 테스트하는 기법이고, 경계값 분석(BVA) 은 그 묶음의 가장자리, 즉 개발자가 < 와 <= 를 헷갈리는 바로 그 지점을 집중 공략하는 기법이다. 둘을 합치면 무한한 입력 공간을 수십 개의 의미 있는 테스트로 줄일 수 있다.
왜 필요한가
나이를 받아 요금을 계산하는 함수가 있다. 입력이 int 라면 가능한 값은 40억 개가 넘는다. 전부 테스트할 수 없다는 것은 ISTQB 테스팅 원칙 2번 “완전한 테스트는 불가능하다” 그대로다. 그렇다고 손에 잡히는 대로 20, 30, 40 을 넣으면 세 테스트가 모두 같은 분기를 지나가고, 정작 18 과 19 사이의 실수는 놓친다.
테스트 설계 기법이 없는 팀에서는 두 가지 증상이 나타난다.
- 중복 테스트: 같은 분기를 지나는 케이스가 수십 개 쌓여 실행 시간만 늘린다.
- 구멍 난 테스트: 경계, 음수, 빈 값, 최댓값 같은 “다른 처리를 받는” 입력이 하나도 없다.
동등 분할은 첫 번째를, 경계값 분석은 두 번째를 해결한다. 둘 다 코드 내부를 보지 않고 명세만으로 테스트를 설계하므로 블랙박스 기법이라 부른다.
핵심 개념
동등 분할의 정의
ISTQB Foundation Level 강의계획서 v4.0.1 4.2.1절의 정의를 요약하면 다음과 같다.
- 데이터를 분할(partition) 로 나눈다. 같은 분할의 원소는 테스트 대상이 모두 같은 방식으로 처리할 것이라는 기대에 근거한다.
- 그 이론은 “분할에서 값 하나를 테스트해 결함을 찾았다면, 같은 분할의 다른 값으로도 그 결함을 찾았을 것” 이라는 가정이다. 그래서 분할당 테스트 하나면 충분하다.
- 분할은 입력뿐 아니라 출력, 설정 항목, 내부 값, 시간 관련 값, 인터페이스 파라미터 에도 만들 수 있다.
- 분할은 서로 겹치지 않고 비어 있지 않아야 한다.
- 처리되어야 할 값의 분할은 유효 분할, 거부·무시되어야 할 값의 분할은 무효 분할 이다.
- 커버리지 = 최소 한 번 실행된 분할 수 / 식별된 전체 분할 수. 100% 에는 무효 분할도 포함된다.
입력이 여럿이면 각 입력의 분할을 최소 한 번씩 밟는 Each Choice 커버리지 가 가장 단순한 기준이다. 강의계획서가 지적하듯 이 기준은 분할 간 조합 을 고려하지 않는다.
무효 분할은 하나씩
Glenford Myers 의 The Art of Software Testing 이 제시한 동등 분할 절차에는 유효 분할은 한 테스트에 여러 개를 함께 담아도 되지만 무효 분할은 테스트 하나에 하나씩만 넣으라는 규칙이 있다. 이유는 가림(masking)이다.
create_user(age=-5, email="not-an-email")
→ ValueError("invalid age") # 나이 검사가 먼저 실패
→ 이메일 검증 로직은 실행조차 안 됨
두 무효 값을 한꺼번에 넣으면 첫 검사에서 튕겨 나가, 두 번째 검사가 아예 없어도 테스트는 통과한다.
경계값 분석: 2값과 3값
경계값 분석은 순서가 있는 분할에만 쓸 수 있다. 분할의 최솟값과 최댓값이 경계값이다. 강의계획서 4.2.2절은 두 변형을 정의한다.
| 변형 | 경계 하나당 커버리지 항목 | 출처로 언급된 문헌 |
|---|---|---|
| 2값 BVA | 경계값, 그리고 인접 분할에 속한 가장 가까운 이웃 | Craig 2002, Myers 2011 |
| 3값 BVA | 경계값, 그리고 양쪽 이웃 둘 | Koomen 2006, O’Regan 2019 |
3값이 더 엄격한 이유를 강의계획서가 예로 보여 준다. if (x ≤ 10) 을 if (x = 10) 으로 잘못 구현했다면 2값 BVA 의 데이터(10, 11)로는 결함이 드러나지 않는다. 10 은 양쪽 모두 참이고 11 은 양쪽 모두 거짓이기 때문이다. 3값 BVA 가 추가하는 9 에서 비로소 차이가 난다.
분할 A (≤10) │ 분할 B (≥11)
... 7 8 9 [10] │ [11] 12 13 ...
▲ ▲ ▲ ▲
3값 추가 ─┘ │ │ └─ 3값 추가
└─2값─┘
카테고리-분할 방법
여러 입력과 환경 조건이 얽힌 기능에서 분할을 체계적으로 만드는 방법으로 Ostrand 와 Balcer 의 카테고리-분할 방법 (CACM, 1988) 이 있다. 테스트 엔지니어가 명세를 분석해 형식적인 테스트 명세를 쓰고, 생성 도구가 거기서 테스트 기술(description)을 만들어 낸다. 명세에 제약(constraint) 을 주석으로 달아 테스트 수와 복잡도를 조절할 수 있다는 것이 논문이 꼽는 장점이다. 실무 용어로 바꾸면 다음과 같다.
- 기능을 독립적으로 테스트 가능한 단위로 나눈다.
- 각 단위의 파라미터와 환경 조건을 카테고리 로 뽑는다(예: “장바구니 상품 수”, “회원 등급”).
- 카테고리마다 선택지(choice) 를 정한다. 동등 분할이 여기 들어간다(0개, 1개, 여러 개, 최대치 초과).
- 말이 안 되는 조합을 제약으로 지운다(
[if 비회원] 등급 = 없음). - 남은 조합에서 테스트 프레임을 생성한다.
실무 적용
명세
입장료: 0~6세 무료, 7~18세 5,000원, 19~64세 10,000원, 65~150세 5,000원. 0 미만이나 150 초과는
ValueError.
분할과 경계
| 분할 | 범위 | 대표값 | 2값 경계 테스트 |
|---|---|---|---|
| 무효(하) | < 0 | -10 | -1 |
| 유아 | 0–6 | 3 | 0, 6 |
| 청소년 | 7–18 | 12 | 7, 18 |
| 성인 | 19–64 | 40 | 19, 64 |
| 경로 | 65–150 | 80 | 65, 150 |
| 무효(상) | > 150 | 200 | 151 |
분할 대표값만 쓰면 6개, 2값 경계값이 10개라 합쳐서 16개다. 40억 개에서 16개로 줄었고, 그 16개가 서로 다른 이유로 존재한다.
출력 쪽 분할도 확인한다. 출력 요금은 0, 5,000, 10,000 세 값뿐이고 5,000 은 서로 떨어진 두 입력 분할에서 나온다. “5,000 이 나오는 두 경로가 모두 테스트되는가” 는 입력 분할 표만 봐서는 놓치기 쉬운 질문이다.
pytest 로 옮기기
import pytest
from fees import ticket_price
@pytest.mark.parametrize("age, expected", [
(0, 0), (3, 0), (6, 0), # 유아: 하한·대표·상한
(7, 5000), (12, 5000), (18, 5000), # 청소년
(19, 10000), (40, 10000), (64, 10000), # 성인
(65, 5000), (80, 5000), (150, 5000), # 경로
])
def test_valid_partitions_and_boundaries(age, expected):
assert ticket_price(age) == expected
@pytest.mark.parametrize("age", [-1, -10, 151, 200]) # 무효 분할: 하나씩
def test_invalid_partitions(age):
with pytest.raises(ValueError):
ticket_price(age)
테스트 ID 에 분할 이름이 드러나게 하면(ids= 인자) 실패 메시지만 보고도 “어느 경계가 깨졌는지” 알 수 있다.
숫자가 아닌 입력의 경계
경계는 정수 범위에만 있지 않다. 순서가 있는 모든 것에 있다.
| 입력 종류 | 경계 후보 |
|---|---|
| 문자열 길이 | 0, 1, 최대 길이, 최대+1 |
| 컬렉션 | 빈 목록, 원소 1개, 페이지 크기, 페이지 크기+1 |
| 날짜·시간 | 월말, 윤년 2월 29일, 자정, 서머타임 전환, 타임존 경계 |
| 금액 | 0, 최소 단위(1원, 0.01달러), 한도, 한도+최소 단위 |
| 부동소수점 | 0.0, -0.0, 아주 작은 양수, NaN, 무한대 |
흔한 오해와 함정
- “분할” 을 코드 분기에서 거꾸로 만든다. 코드를 보고
if마다 분할을 만들면 코드에 없는 분기(명세엔 있는데 구현이 빠뜨린 처리)는 영원히 테스트되지 않는다. 분할은 명세에서 만든다. - 같은 분할이라는 가정을 검증하지 않는다. 동등 분할은 “같게 처리될 것” 이라는 기대에 기댄 기법이다. 구현이 내부적으로
age < 10같은 숨은 분기를 두면 그 가정은 깨진다. 이런 곳은 화이트박스 기법(SE100 #043)이나 속성 기반 테스트(SE100 #046)로 보완한다. - 무효 값을 한 테스트에 몰아넣기. 앞의 가림 문제 때문에 검증 로직 하나가 통째로 빠져도 통과한다.
- 조합을 Each Choice 로 끝내기. 입력 간 상호작용이 있는 결함은 Each Choice 로 잡히지 않는다. 결정 테이블이나 조합(pairwise) 기법이 필요하다.
- 경계 근처 하나만 찍기. 2값과 3값 중 무엇을 쓸지는 위험도로 정한다. 요금·권한·한도처럼 틀리면 비싼 경계에는 3값이 값싸게 안전을 산다.
확인 문제
- 동등 분할이 “분할당 테스트 하나” 를 정당화하는 가정은 무엇인가?
- 무효 분할을 한 테스트에 하나씩만 넣어야 하는 이유를 예로 설명하라.
if (x ≤ 10)이if (x == 10)으로 잘못 구현되었다. 2값 BVA 와 3값 BVA 의 테스트 데이터를 각각 쓰고, 어느 쪽이 결함을 드러내는지 보여라.- 문자열 필드(최대 255자)의 길이에 대한 2값 경계 테스트를 나열하라.
- 코드의
if문에서 분할을 도출하면 안 되는 이유는?
풀이
- 같은 분할의 값들은 테스트 대상이 같은 방식으로 처리한다는 기대다. 따라서 한 값에서 드러나는 결함은 같은 분할의 다른 값에서도 드러난다고 본다.
- 첫 번째 무효 값에서 검증이 실패하면 나머지 검증 로직은 실행되지 않는다. 나이와 이메일이 모두 무효인 입력은 이메일 검증이 아예 없어도 예외가 나므로 테스트가 통과한다.
- 2값: 10, 11. 10 은 원래 식과 잘못된 식 모두 참, 11 은 모두 거짓이라 결함이 안 드러난다. 3값: 9, 10, 11. 9 에서 원래 식은 참, 잘못된 식은 거짓이므로 결함이 드러난다.
- 빈 문자열을 허용한다면 유효 분할은 0–255 이고 하한 아래 이웃(-1)은 만들 수 없으므로 0, 255, 256. 최소 1자를 요구한다면 0, 1, 255, 256.
- 명세에는 있는데 구현이 빠뜨린 처리는 코드에 분기가 없으므로 분할도 생기지 않는다. 블랙박스 기법의 가치는 명세 기준으로 구현의 누락을 찾는 데 있다.
더 읽을거리 (References)
- ISTQB, Certified Tester Foundation Level Syllabus v4.0.1 (PDF), 4.2.1–4.2.2절
- T. J. Ostrand, M. J. Balcer, The Category-Partition Method for Specifying and Generating Functional Tests, Communications of the ACM 31(6), 1988
- Glenford J. Myers, Corey Sandler, Tom Badgett, The Art of Software Testing, 3rd ed., Wiley, 2011 (서지 정보)
- Paul Ammann, Jeff Offutt, Introduction to Software Testing, 2nd ed., Cambridge University Press, 2016 (서지 정보)