테스트 설계
테스트 케이스는 스위트가 무엇을 잡을 수 있는지 결정합니다. 그 이후의 모든 것 — 테스트 도구, 러너, 개수 —은 단지 그 결정을 실행할 뿐입니다.
이 기술이 대체하는 순간들
| ~하려 할 때… | 대신… |
|---|---|
| 해피 패스를 작성하고 멈춤 | 입력 공간을 탐색: 각 파라미터에 대해 클래스와 경계를 식별 |
| 구현에서 테스트 케이스 도출 | 명세서에서 도출하고, 구현이 당신을 놀라게 하도록 둠 |
| 편안한 값으로 테스트 | 각 경계의 안쪽 마지막 값과 바깥쪽 첫 번째 값을 테스트 |
| 명세서가 명명한 경계만 테스트 | 모든 범위에는 두 끝이 있음; 문장에서 빠진 끝도 여전히 경계임 |
| 명세서가 나열한 역할만 열거 | 잊혀진 호출자를 추가: 역할 없음, 인증되지 않음, 할당되지 않음 |
| 해피 패스에서 하나의 조건만 변경 | 결정 테이블을 만들고 두 조건이 함께 실패하는 규칙을 커버 |
| 상태 테이블을 그리고 넘어감 | 모든 행에 테스트 ID를 넣음; 특히 "허용되지 않음" 행에 가장 중요 |
| 예상 결과에 "또는 거부됨, 단..."을 작성 | 케이스당 하나의 결과; 결정하지 못하는 명세서는 테스트가 아닌 미해결 질문 |
| 오류 경로를 가능성 없음으로 건너뜀 | 각 실패 모드는 하나의 클래스: 잘못된 입력, 누락된 리소스, 거부됨, 타임아웃, 미완료 |
| 구현되지 않은 경로를 테스트 없이, 스킵 마커나 단언 없는 스텁으로 남김 | 해당 경로가 오늘날 구현되지 않은 것으로 거부된다는 테스트를 작성; 지금은 통과하고 누군가 구현하는 날 빨간색이 됨 (아래 참조) |
| 기능을 주요 흐름으로만 테스트 | 사용 사례에서 대체 및 예외 흐름을 읽어냄; 각각은 하나의 클래스 |
| 스위트를 읽고 등급을 매김 | 명세서와 대조하여 계산: 클래스, 양쪽 경계, 규칙, 전이 |
| 이미 커버된 클래스에 다섯 번째 테스트 추가 | 커버되지 않은 클래스에 사용 — 다섯 번째는 실행 시간만 소모하고 커버리지는 제공하지 않음 |
명세서를 공격하고, 구현을 공격하지 마라
코드를 읽고 작성된 테스트는 코드가 하는 일을 단언합니다. 도착하자마자 통과하고, 현재 버그를 기대치로 인코딩하며, 변경 후 실패는 "코드가 변경되었다"는 뜻인데, 이는 이미 알려진 사실입니다. 이는 측정된 것이지 추측이 아닙니다: 24개의 Java 저장소에서 LLM 생성 오라클은 예상된 동작보다 프로그램의 실제 동작을 포착하는 경향이 있었습니다 (Konstantinou, Degiovanni, Papadakis, arXiv:2410.21136).
해야 할 일에서 도출하세요 — 명세서, 와이어 포맷, 계약 — 그리고 그런 다음에만 실행하세요. 유일하게 허용되는 예외는 의도적으로 현재 동작을 고정하는 특성화 테스트이며, 이는 나중에 누군가 고정된 버그를 요구사항으로 착각하지 않도록 표시됩니다.
유용한 징후: 모든 테스트가 첫 실행에 초록색이었다면, 의도와 상관없이 스위트는 구현에서 도출된 것입니다.
동치 클래스: 테스트가 아닌 클래스를 세십시오
코드가 구분할 수 없는 입력은 하나의 클래스를 형성하며, 클래스당 하나의 테스트면 충분합니다 — 두 번째부터 다섯 번째는 실행 시간만 소모하고 새로운 것을 증명하지 않습니다. 규율은 양방향으로 작동합니다: 어떤 클래스도 커버되지 않은 채로 두지 않고, 어떤 클래스도 실수로 두 번 커버하지 않습니다.
열일곱 개의 테스트로 구성된 스위트는 철저해 보입니다. 그러나 철저한지 여부는 그 열일곱 개가 몇 개의 클래스에 속하는지에 달려 있으며, 그 숫자는 어떤 러너의 출력에도 나타나지 않습니다. 입력 공간과 대조하여 수동으로 계산해야 합니다 — 이것이 바로 테스트 계획입니다.
명세서가 나열하는 클래스는 쉬운 절반입니다. 세 개의 행이 있는 역할 테이블에는 네 번째 클래스가 있습니다: 그 중 어느 것과도 일치하지 않는 요청이며, 테이블에서 구축된 스위트에는 절대 포함되지 않는 클래스입니다. 각 파라미터에 대해 케이스를 작성하기 전에 유효하지 않은 클래스를 유효한 클래스 옆에 기록하세요: 잘못된 유형, 누락, 나열된 모든 값 밖, 권한이 전혀 없는 호출자.
교과서는 이를 습관이 아닌 규칙으로 명시합니다: 동치 분할에서 커버리지 항목은 분할이며, 100%는 식별된 모든 분할이 "유효하지 않은 분할을 포함하여" 실행되었음을 의미하며, 분할은 "겹치지 않아야 하고 비어 있지 않은 집합이어야 합니다" (ISTQB CTFL v4.0.1 §4.2.1). 여러 파라미터가 있는 경우 가장 약한 기준인 각 선택(Every Choice) — 각 파라미터의 모든 분할을 한 번씩, 조합은 무시 — 을 명명하며, 이는 대부분의 스위트가 이름을 모르고 도달하는 수준이며, 아래 결정 테이블이 시작되는 지점입니다.
경계, 바로 거기에 결함이 있기 때문입니다
경계 오차는 가장 흔한 산술 결함이며,
경계 외에는 어디에서도 보이지 않습니다.
각 경계에서 중요한 쌍은 경계값과 경계선을 넘는 가장 가까운 이웃이며,
이는 2-값 BVA입니다 (CTFL v4.0.1 §4.2.2);
3-값 BVA는 내부의 이웃도 추가하며,
교과서는 이것이 무엇을 제공하는지 보여줍니다:
x ≤ 10을 의도했지만 x = 10으로 작성된 검사는 10과 11을 모두 통과하며,
9만이 그것을 잡아냅니다.
반복되는 경계: 비어 있음, 정확히 하나, 많음; 0과 음수;
선언된 최대값과 그 하나를 넘는 값;
정확히 버퍼, 한계, 타임아웃인 값.
이 머신의 구체적인 쌍: 8,190자에서는 괜찮고 8,192자에서는 반드시 실패해야 하는 PATH 검사는 두 값에서 테스트되어야 합니다. 4,000과 12,000에서 통과하는 것은 그 사이의 선에 대해 거의 아무것도 확립하지 않습니다.
경계가 명시된 전제 조건인 경우,
그 값을 넘는 것은 예상 결과가 있는 케이스보다는 호출자의 버그입니다;
design-by-contract는 주어진 경계가 둘 중 어느 것인지 결정하며,
테스트는 그에 해당하는 것을 단언합니다.
명세서가 명명하지 않은 쪽
"최대 10MB"는 범위의 한 끝을 명명합니다. 다른 끝은 문장이 언급하든 말든 존재하며, 텍스트에서 도출된 케이스는 텍스트가 멈추는 곳에서 멈춥니다: 상한을 -1, 0, +1 바이트에서 테스트하고 1,024 이하는 전혀 테스트하지 않는 스위트는 범위의 절반에 대해 경계 분석을 수행한 것입니다. 정의에는 쪽이 없습니다: "분할의 최소값과 최대값이 경계값입니다" (CTFL v4.0.1 §4.2.2). 먼저 모든 분할에 대해 두 값을 모두 기록한 다음, 각각을 인접 분할의 가장 가까운 이웃과 함께 취합니다. 명명되지 않은 쪽은 일반적으로 0, 비어 있음 또는 1이며, 아무도 결정하지 않았기 때문에 명세서는 일반적으로 이에 대해 침묵합니다.
두 조건을 동시에 움직이세요
해피 패스에서 하나의 조건만 변경하여 구축된 스위트는 모든 조건의 모든 값을 커버하지만 거의 어떤 조합도 커버하지 않습니다: 형식 오류, 크기 초과, 개수 초과, 각각 단독으로, 그리고 두 가지가 함께 실패하는 경우는 없습니다. 둘 다 적용될 때 어떤 오류가 우선하는지도 명세서에는 거의 없으므로, 누락된 케이스는 누락된 결정을 숨깁니다.
조건을 결정 테이블로 배치하고 규칙을 세고,
그런 다음 두 조건이 동시에 실패하는 규칙과 역할 제한 및 형식 제한이 모두 적용되는 규칙을 커버합니다.
결정 테이블 테스트에서 커버리지 항목은 실현 가능한 조합을 보유한 열이며,
100%는 그러한 모든 열이 실행되었음을 의미합니다 (CTFL v4.0.1 §4.2.3);
교과서는 테이블이 "요구사항의 갭이나 모순을 찾는 데 도움이 된다"고 덧붙이며,
이는 위의 누락된 결정이 스스로 표면화되는 것입니다.
동일한 문서가 각 선택(Every Choice)에서는 완전히 커버된 것으로 읽히고 열에서는 1/4만 커버된 것으로 읽힐 수 있으며,
두 번째 숫자만이 조합에 대해 무언가 말해줍니다.
규칙 개수가 플래그 집합의 곱일 때,
더 저렴한 수정은 상류에 있습니다:
state-first는 플래그를 발생할 수 있는 상태로 축소하고,
테이블은 그 상태들로 축소됩니다.
테이블은 스위트에 대한 주장입니다
상태 테이블, 결정 테이블, 사용 사례의 흐름 목록: 각 행은 해당 행에 대한 테스트가 존재한다고 주장합니다. 행에 테스트 ID를 넣으세요. ID가 없는 행은 커버되지 않은 클래스이며, "허용되지 않음"이라고 읽히는 행이 가장 빈 칸일 가능성이 높습니다. 왜냐하면 금지를 작성하는 것이 처리한 것처럼 느껴지기 때문입니다. 금지된 전환 — 하나가 진행 중일 때 두 번째 전송 — 은 테스트이며, 이를 중지해야 하는 가드가 테스트 대상입니다.
상태 테이블은 해당 행을 명시적으로 만듭니다: 유효하지 않은 전환은 빈 셀이며, 교과서가 정의하는 세 가지 기준 중 가장 강력한, 모든 전환 커버리지(all transitions coverage)는 모든 전환을 시도해야 합니다 — 테스트 케이스당 하나씩, 하나의 결함이 다른 결함을 가릴 수 없도록 (CTFL v4.0.1 §4.2.4). 유효한 전환 커버리지(valid transitions coverage), 대부분의 스위트가 멈추는 0-스위치는 절대 그 셀을 건드리지 않으며, 이것이 테스트되지 않은 "허용되지 않음" 행이 초록색으로 유지되는 방법입니다.
그런 다음 두 문서를 서로 대조하여 읽으세요. 테이블과 경로가 일치하지 않는 테스트 (테이블은 실패 → 전송 중이라고 하고, 테스트는 실패 → 선택됨 → 전송 중으로 감)는 둘 중 하나가 잘못되었음을 의미하며, 비교만이 어느 쪽인지 알아냅니다. 자기 전환 및 실패 상태에서 나가는 전환도 행으로 계산됩니다.
케이스당 하나의 결과
"거부됨, 또는 수락된 경우 전송 후 손상된 데이터 없음"은 통과하거나 실패할 수 없습니다. 이는 명세서가 0바이트 파일에 무슨 일이 일어나는지 말하지 않고 작성자가 답을 만들기를 거부했을 때 정직하게 나타나며, 케이스가 눈에 띄게 존재하기 때문에 검토에서 살아남습니다. 분리는 예상 열에 숨어 있습니다. 올바른 형태는 명세서 소유자에게 질문하는 것이며, 그렇게 나열되고, 답변될 때까지 해당 클래스에 대한 테스트는 없습니다.
오류 경로도 클래스입니다
불행한 경로는 테스트되지 않은 코드가 축적되는 곳입니다. 정상 사용에서는 아무것도 실행하지 않기 때문입니다 — 잠금 소유자가 실행 중에 죽는 경우, 존재하지 않는 매핑된 폴더, 예상된 필드가 누락된 입력. 각각은 모든 입력 범위와 동일한 테스트 주장을 가진 동치 클래스입니다. 모든 것이 협력할 때만 도구가 작동함을 증명하는 스위트는 데모를 테스트한 것이지, 도구를 테스트한 것이 아닙니다.
사용 사례는 코드가 있기 전에 클래스를 열거합니다
사용 사례는 이미 클래스로 나누어진 명세서입니다: 하나의 주요 흐름, 각 대체 흐름, 각 예외 흐름, 그리고 각각이 입력될 수 있는 전제 조건. 이를 작성한 사람은 이 기술이 요구하는 열거를 이미 수행했습니다 — 대체 및 예외 흐름은 오류 경로입니다 — 따라서 주요 흐름을 실행하는 스위트는 문서가 나열하는 N개의 클래스 중 하나를 커버한 것입니다.
사용 사례에서 흐름을 읽고 각각에 테스트를 할당한 다음, 전제 조건을 확인하세요. 흐름은 전제 조건이 유지될 수 있을 때만 도달 가능하기 때문입니다. 번역된 페이지에 대한 패리티 검사는 주요 흐름(두 언어 모두 편집 및 커밋됨), 대체 흐름(예외로 선언된 한쪽 편집), 그리고 전제 조건이 기록된 후에만 나타나는 두 개의 예외 흐름이 있습니다: 트리가 더럽기 때문에 아직 부실을 판단할 수 없음; 히스토리가 다시 작성되었기 때문에 선언된 커밋이 더 이상 확인되지 않음. 둘 다 일주일 이내에 한 사이트에서 발생했습니다. 해피 패스만으로는 둘 다 보여주지 못했을 것입니다.
예외 흐름이 실제로 발생할 수 있는지 여부는 얼마나 가능성이 없어 보이는지로 결정되지 않습니다.
운영 범위(operating envelope)는 범위 밖의 것을 말하고 위험 분석(hazard analysis)은 남은 것을 말합니다;
둘 다 assurance-case에 있으며,
이 기술은 직관으로 가지치기하는 대신 그들이 생성하는 목록을 사용합니다.
커버리지는 명명된 기준 아래의 숫자입니다
하나의 테스트 문서가 세 가지 방법으로 측정됨: 조건의 100%, MC/DC의 90.9%,
결정 규칙의 25%.
세 가지 모두 동일한 파일에 대해 참이므로,
기준 없이 커버리지 수치는 아무것도 말하지 않으며,
카운트 후에 기준이 선택된 수치는 더 적게 말합니다.
카운트하기 전에 기준을 명명하세요,
교과서의 용어로 숫자를 확인할 수 있도록:
분할 커버리지 및 각 선택(Every Choice) (§4.2.1),
2-값 또는 3-값 BVA (§4.2.2), 결정 테이블 열 (§4.2.3),
모든 상태, 유효한 전환 또는 모든 전환 (§4.2.4).
MC/DC는 기초 교과서에 없습니다;
그것은 안전 표준(DO-178C, ISO 26262)에서 비롯되었으며,
그것을 말하지 않고 다른 것 옆에 나열하는 것은 엄격함으로 읽히는 혼합 인용 유형입니다.
숫자가 독자에게 빚진 나머지는 measured-claims에 있습니다.
스위트 확인: 판단하지 말고 계산하세요
스위트가 적절한지 묻는 것은 그것을 생성한 맹점을 반환합니다.
동일한 모델에 대한 두 번째 세션은 그 맹점을 공유하며,
하루 후의 동일한 사람도 마찬가지입니다.
교과서는 이를 "약간의 독립성(some independence)"라고 부르며,
네 가지 수준 중 두 번째이며, 가치가 오는 곳을 말합니다:
"작성자와 테스터의 인지 편향 간의 차이" (CTFL v4.0.1 §1.5.3).
동일한 모델에 대한 두 번째 세션은 편향을 공유하고 레이블만 유지합니다.
편향을 공유하지 않는 단계는 계산하는 것입니다:
명세서에서 클래스를 나열하고 테스트가 있는 것에 체크 표시하고,
모든 범위의 양쪽 경계를 나열하고, 결정 테이블의 규칙을 나열하고,
모든 상태 테이블의 행을 나열하고,
각 갭의 파일과 줄을 인용하세요.
예상 값이 올바른지 여부는 여전히 판단이며 그것을 작성하지 않은 독자에게 맡겨집니다;
클래스에 테스트가 있는지 여부는 산술입니다.
카운트가 철저하지 않고 샘플링된 경우,
보고서는 샘플링되었다고 말합니다.
평결에 대한 반박-기본 형식은 adversarial-verify에 있습니다;
이 기술은 무엇을 계산할지 제공합니다.
요청이 클래스를 제거할 때
"이중 전송은 버튼이 비활성화되어 있으므로 발생할 수 없습니다. 해당 행을 삭제하세요"는 한 레이어의 하나의 가드의 강점으로 스위트에서 클래스를 제거합니다. 요청된 대로 하고, 어떤 클래스가 이제 커버되지 않으며 무엇이 그것에 도달할 수 있는지 한 문장으로 말하세요: 상태 머신은 버튼이 아닌 모든 호출자로부터 두 번째 전송을 여전히 수락합니다. 그 문장과 함께 삭제된 행은 결정입니다; 그 문장 없이 삭제된 행은 아무도 다시 찾지 못할 갭입니다.
구현되지 않은 경로에는 그렇게 말하는 테스트가 있습니다
이 변경에서 의도적으로 제외된 경로,
이 릴리스에서 지원하지 않는 역할, 아직 구문 분석되지 않은 형식,
NotImplementedException을 던지거나 todo!()를 호출하는 분기는
여전히 입력의 클래스이며,
스위트는 오늘날 그곳에서 무슨 일이 일어나는지 기록합니다.
경로가 구현되지 않은 것으로 거부된다는 테스트를 작성하세요:
그것이 발생시키는 예외 또는 오류 값,
없는 메뉴 항목, "지원되지 않음"이라고 응답하는 요청.
그 테스트는 지금 통과하며 두 가지 작업을 수행합니다. 그것은 스위트 목록에 갭을 넣으며, 클래스를 세는 독자가 찾을 수 있도록, 주석이나 티켓 대신에. 그리고 누군가 경로를 구현하는 날 빨간색이 되며, 이는 실제 케이스를 작성해야 하는 순간입니다; 실패하는 테스트는 알림이며, 삭제되지 않고 대체됩니다.
스킵 마커나 보류 중인 테스트는 위의 작업을 수행하지 않습니다; 그것은 갭을 문서화하지 않으며 도달할 때 실패하지 않습니다.
측정된 주장을 보십시오
이 기술은 테스트 설계 기술을 다룹니다. 측정 및 커버리지 주장 자체는 별도의 세션에 속합니다.