소프트웨어 공학 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) 을 주석으로 달아 테스트 수와 복잡도를 조절할 수 있다는 것이 논문이 꼽는 장점이다. 실무 용어로 바꾸면 다음과 같다.

  1. 기능을 독립적으로 테스트 가능한 단위로 나눈다.
  2. 각 단위의 파라미터와 환경 조건을 카테고리 로 뽑는다(예: “장바구니 상품 수”, “회원 등급”).
  3. 카테고리마다 선택지(choice) 를 정한다. 동등 분할이 여기 들어간다(0개, 1개, 여러 개, 최대치 초과).
  4. 말이 안 되는 조합을 제약으로 지운다([if 비회원] 등급 = 없음).
  5. 남은 조합에서 테스트 프레임을 생성한다.

실무 적용

명세

입장료: 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값이 값싸게 안전을 산다.

확인 문제

  1. 동등 분할이 “분할당 테스트 하나” 를 정당화하는 가정은 무엇인가?
  2. 무효 분할을 한 테스트에 하나씩만 넣어야 하는 이유를 예로 설명하라.
  3. if (x ≤ 10) 이 if (x == 10) 으로 잘못 구현되었다. 2값 BVA 와 3값 BVA 의 테스트 데이터를 각각 쓰고, 어느 쪽이 결함을 드러내는지 보여라.
  4. 문자열 필드(최대 255자)의 길이에 대한 2값 경계 테스트를 나열하라.
  5. 코드의 if 문에서 분할을 도출하면 안 되는 이유는?

풀이

  1. 같은 분할의 값들은 테스트 대상이 같은 방식으로 처리한다는 기대다. 따라서 한 값에서 드러나는 결함은 같은 분할의 다른 값에서도 드러난다고 본다.
  2. 첫 번째 무효 값에서 검증이 실패하면 나머지 검증 로직은 실행되지 않는다. 나이와 이메일이 모두 무효인 입력은 이메일 검증이 아예 없어도 예외가 나므로 테스트가 통과한다.
  3. 2값: 10, 11. 10 은 원래 식과 잘못된 식 모두 참, 11 은 모두 거짓이라 결함이 안 드러난다. 3값: 9, 10, 11. 9 에서 원래 식은 참, 잘못된 식은 거짓이므로 결함이 드러난다.
  4. 빈 문자열을 허용한다면 유효 분할은 0–255 이고 하한 아래 이웃(-1)은 만들 수 없으므로 0, 255, 256. 최소 1자를 요구한다면 0, 1, 255, 256.
  5. 명세에는 있는데 구현이 빠뜨린 처리는 코드에 분기가 없으므로 분할도 생기지 않는다. 블랙박스 기법의 가치는 명세 기준으로 구현의 누락을 찾는 데 있다.

더 읽을거리 (References)