본문 바로가기

문장·분기·조건 검증

문장, 분기, 조건, 경로 커버리지가 각각 무엇을 세는지 구별하고, 단락 평가 때문에 실제로 평가되지 않는 조건과 MC/DC의 독립 효과를 실행으로 추적합니다. 제어 흐름 그래프에서 순환 복잡도를 계산해 기본 경로의 사례를 고르고, Node.js의 커버리지 보고를 요구와 함께 읽습니다.

정원이 차서 거절되는 경로를 한 번 실행했다는 것과 모든 거절 이유를 확인했다는 것은 다릅니다. 커버리지(coverage)는 정해 둔 확인 대상 가운데 테스트가 실행하거나 만족한 것의 비율입니다. 무엇을 셀지 먼저 정하지 않으면 "커버리지 100%"라는 말은 뜻이 모호해지므로, 이 글에서는 코드의 구조를 기준으로 하는 여러 커버리지가 각각 무엇을 세는지 구별하고, 작은 코드에서 직접 세어 봅니다.

무엇을 세는지에 따라 달라지는 커버리지

문장 커버리지(statement coverage)는 실행할 수 있는 문장이 한 번 이상 실행되었는지를, 분기 커버리지(branch coverage)는 if 같은 결정의 참과 거짓 경로가 모두 실행되었는지를 셉니다. 조건 커버리지(condition coverage)는 a && b처럼 여러 조건으로 이루어진 결정에서 각 조건이 참과 거짓으로 모두 평가되었는지를 봅니다. 경로 커버리지(path coverage)는 분기의 조합으로 생기는 모든 실행 경로를 다루지만, 반복문이 있으면 경로의 수가 매우 많아지거나 끝없이 늘어날 수 있습니다.

결정 x > 1 && y === 0을 예로 들어 보겠습니다. 다음 코드는 두 사례에서 각 조건의 논리값과 결정의 값을 따로 계산해 출력합니다.

const cases = [[2, 0], [1, 1]];
for (const [x, y] of cases) {
  const a = x > 1;
  const b = y === 0;
  console.log({ x, y, a, b, decision: a && b });
}
{ x: 2, y: 0, a: true, b: true, decision: true }
{ x: 1, y: 1, a: false, b: false, decision: false }

두 사례로 결정은 참과 거짓을 모두 얻었으므로 분기 커버리지를 만족합니다. 각 조건도 표에서는 참과 거짓을 모두 가졌습니다. 하지만 이 코드는 설명을 위해 b를 미리 계산했을 뿐이고, 원래의 식을 그대로 실행하면 사정이 다릅니다. &&는 앞 조건이 거짓이면 결과가 이미 거짓으로 정해지므로 뒤 조건을 계산하지 않는데, 이를 단락 평가(short-circuit evaluation)라고 합니다.

실제로 평가된 조건을 추적한다

어떤 조건이 실제로 평가되었는지 알아보려면, 조건을 평가할 때마다 기록을 남기면 됩니다. 다음 코드를 reading-lab/examples/ch5-3.mjs로 저장하고 실행합니다.

function decide(x, y) {
  const evaluated = [];
  const A = () => {
    evaluated.push("A");
    return x > 1;
  };
  const B = () => {
    evaluated.push("B");
    return y === 0;
  };
  const decision = A() && B();
  return { decision, evaluated: evaluated.join(" ") };
}

for (const [x, y] of [[2, 0], [1, 1], [2, 1]]) {
  const { decision, evaluated } = decide(x, y);
  console.log(x, y, "결정:", decision, "평가한 조건:", evaluated);
}
2 0 결정: true 평가한 조건: A B
1 1 결정: false 평가한 조건: A
2 1 결정: false 평가한 조건: A B

[1, 1]에서는 A가 거짓이라 B를 평가하지 않았습니다. 그래서 처음의 두 사례만으로는 B가 실제로 평가된 것은 참일 때뿐이며, B가 거짓으로 평가되는 경우는 [2, 1]을 더해야 생깁니다. 분기 커버리지를 만족했다고 해서 각 조건이 참과 거짓으로 실제로 평가되었다고 볼 수는 없는 것입니다.

조건 하나가 결정을 바꾸는지 본다

조건마다 참과 거짓이 나왔는지를 넘어, 그 조건 하나만 바뀌었을 때 결정이 실제로 바뀌는지도 확인할 수 있습니다. 아래 그림처럼 [2, 0]을 기준 사례로 두고, [1, 0]은 B의 논리값을 참으로 둔 채 A만 바꾼 사례, [2, 1]은 A를 참으로 둔 채 B만 바꾼 사례로 비교합니다. 두 비교에서 모두 결정이 참에서 거짓으로 바뀌었으므로, A와 B가 각자 결정에 독립적으로 영향을 준다는 것을 확인한 셈입니다.

SOFTWARE ENGINEERING · READING CLUB

같은 기준 사례에서 조건 하나씩 바꾸어 비교한다

A: x > 1, B: y === 0, 결정: A && B. 각 비교는 첫 기준 사례와 연결합니다.

같은 기준 사례에서 조건 하나씩 바꾸어 비교한다기준 [2,0]은 A참 B참 결정참입니다. 기준과 [1,0]은 B의논리값을참으로고정하고 A만바꿉니다. 기준과 [2,1]은 A를참으로고정하고 B만바꿉니다. 뒤두사례를 서로비교하는것은 독립조건효과비교가아닙니다.기준 [2, 0]A 참 · B 참결정 참A만 변경 [1, 0]A 거짓 · B 논리값 참결정 거짓 / B 평가 생략B만 변경 [2, 1]A 참 · B 거짓결정 거짓 / B 실제 평가B 논리값 고정A 참 고정
같은 기준 사례에서 조건 하나씩 바꾸어 비교한다기준 [2,0]은 A참 B참 결정참입니다. 기준과 [1,0]은 B의논리값을참으로고정하고 A만바꿉니다. 기준과 [2,1]은 A를참으로고정하고 B만바꿉니다. 뒤두사례를 서로비교하는것은 독립조건효과비교가아닙니다.기준 [2, 0]A 참 · B 참결정 참A만 변경 [1, 0]A 거짓 · B 논리값 참결정 거짓 / B 평가 생략B만 변경 [2, 1]A 참 · B 거짓결정 거짓 / B 실제 평가B 논리값 고정A 참 고정

단락 평가에서는 A가 거짓이면 B를 실행하지 않습니다. 논리값 비교와 실제 조건 평가의 커버리지를 구분합니다.

이처럼 각 조건이 다른 조건과 관계없이 결정의 결과를 바꾼다는 것을 보이는 기준을 MC/DC(modified condition/decision coverage)라고 합니다. 항공기 소프트웨어처럼 안전이 중요한 분야에서 요구하는 기준이며, 조건이 n개인 결정은 보통 n+1개 정도의 사례로 만족할 수 있습니다. 다만 [1, 0]과 [2, 1]을 서로 비교하는 것은 두 조건이 동시에 바뀌므로 독립 효과의 비교가 아니며, 실제로 적용할 때는 단락 평가를 어떻게 다룰지까지 기준을 정해야 합니다.

더 깊이: MC/DC를 요구하는 곳과 두 가지 해석

MC/DC가 정확히 어디서 요구되는지는 NASA가 펴낸 MC/DC 안내서(NASA/TM-2001-210876)에 정리되어 있습니다. 항공기 소프트웨어의 인증 지침인 DO-178B는 요구가 가장 엄격한 A등급 소프트웨어에만 MC/DC를 요구하고, B등급까지는 결정 커버리지(decision coverage)를, C등급까지는 문장 커버리지를 요구합니다. 결정 커버리지는 모든 결정이 참과 거짓의 결과를 한 번 이상 내었는지를 세므로 앞의 분기 커버리지와 비슷합니다. 다만 안내서는 결정을 if 같은 분기점에 있는 것으로 한정하지 않고, 대입문 안의 논리식도 결정으로 봅니다. 안내서는 모든 조합을 시험하는 다중 조건 커버리지가 조건이 n개일 때 2ⁿ개의 사례를 요구해 현실적이지 않은 반면, MC/DC는 대개 n+1개 이상의 사례로 각 조건의 독립된 영향을 보일 수 있다는 점을 MC/DC를 쓰는 이유로 듭니다.

독립된 영향을 보이는 방법에도 두 가지 해석이 있습니다. 본문처럼 한 조건만 바꾸고 나머지는 모두 그대로 둔 두 사례를 짝짓는 것을 고유 원인(unique-cause) MC/DC라고 합니다. 그런데 (A && B) || (A && C)처럼 같은 조건이 여러 번 나오거나 조건끼리 강하게 묶인 결정에서는 이렇게 짝지을 수 없으므로, 안내서는 결정의 논리를 분석해 다른 조건 때문에 결과가 가려지는 사례를 빼고 판단하는 마스킹(masking) MC/DC도 받아들이며, 두 방식이 같은 종류의 오류를 찾는다고 설명합니다. 안내서는 Ada의 and then과 or else, C와 C++의 &&와 || 같은 단락 평가의 연산자를 따로 다루는데, JavaScript의 &&와 ||도 같은 방식으로 오른쪽 조건을 건너뜁니다.

제어 흐름 그래프와 순환 복잡도

제어 흐름 그래프(control flow graph)는 함께 실행되는 문장의 묶음을 노드로, 실행이 옮겨 갈 수 있는 길을 간선으로 그린 그래프입니다. 순환 복잡도(cyclomatic complexity)는 이 그래프에서 서로 독립적인 경로가 몇 개인지를 나타내는 지표로, 간선의 수를 E, 노드의 수를 N, 서로 떨어진 그래프 조각의 수를 P라고 하면 V = E - N + 2P로 계산합니다. 함수 하나의 그래프라면 P는 1입니다.

참가자 수와 정원을 받아 자리 여부를 알려 주는 seatText와, 여기에 모집 상태를 더한 admissionText로 계산해 보겠습니다. 참가자 수는 0 이상의 정수, 정원은 1 이상의 정수라고 전제합니다.

function seatText(count, capacity) {
  if (count >= capacity) return "FULL";
  return "AVAILABLE";
}

function admissionText(status, count, capacity) {
  if (status !== "OPEN") return "CLOSED";
  if (count >= capacity) return "FULL";
  return "AVAILABLE";
}

console.log(seatText(2, 2), seatText(1, 2));
console.log(admissionText("CLOSED", 0, 2), admissionText("OPEN", 2, 2), admissionText("OPEN", 1, 2));
FULL AVAILABLE
CLOSED FULL AVAILABLE

seatText의 그래프는 판정 노드 1, FULL을 돌려주는 노드 2, AVAILABLE을 돌려주는 노드 3, 두 반환이 만나는 종료 노드 4로 그릴 수 있습니다. 간선은 1→2, 1→3, 2→4, 3→4의 네 개이므로 V = 4 - 4 + 2 = 2입니다. 서로 독립적인 기본 경로(basis path)는 1→2→4와 1→3→4이고, 각각 (2, 2)와 (1, 2)로 실행할 수 있습니다. 첫 사례만 있었다면 자리가 남는 경로는 한 번도 실행되지 않았을 것입니다.

admissionText는 판정이 두 개라 그래프가 조금 커집니다. 첫 if가 노드 1, 둘째 if가 노드 2이고, CLOSED, FULL, AVAILABLE을 돌려주는 곳이 각각 노드 3, 4, 5입니다. 노드 6은 코드에 적힌 명령이 아니라 모든 반환이 합쳐지는 개념적인 종료점입니다. 아래 그림은 열린 모임에 참가자 수가 정원과 같은 입력이 이 그래프를 어떻게 지나는지 한 단계씩 보여 줍니다.

조건 두 개가 반환 경로 세 개를 나눈다

OPEN이 아닌지를 검사한다. 반환하면 호출이 끝난다. 각 단계의 값과 전환 이유를 비교합니다.

SOFTWARE ENGINEERING · READING CLUB

조건 두 개가 반환 경로 세 개를 나눈다

숫자는 본문의 admission 코드와 대응합니다. 조건이 거짓일 때만 다음 조건으로 진행합니다.

조건 두 개가 반환 경로 세 개를 나눈다노드1 status가 OPEN이 아닌지 판정: 참이면3 CLOSED 반환, 거짓이면2 정원 판정. 노드2 참가수 이상 정원: 참이면4 FULL 반환, 거짓이면5 AVAILABLE 반환. 3,4,5 모두 개념적 종료6에 합류. 여섯 노드와 일곱 변. 1 status!== OPEN?2 count>= capacity?3 returnCLOSED4 returnFULL5 returnAVAILABLE6 종료개념적 합류TFTF
조건 두 개가 반환 경로 세 개를 나눈다노드1 status가 OPEN이 아닌지 판정: 참이면3 CLOSED 반환, 거짓이면2 정원 판정. 노드2 참가수 이상 정원: 참이면4 FULL 반환, 거짓이면5 AVAILABLE 반환. 3,4,5 모두 개념적 종료6에 합류. 여섯 노드와 일곱 변. 1 status!== OPEN?2 count>= capacity?3 returnCLOSED4 returnFULL5 returnAVAILABLE6 종료개념적 합류TFTF

마름모는 조건, 사각형은 반환, 더 둥근 종료 상자는 개념적 합류입니다. 화살표는 가능한 제어 이동, T/F는 조건의 참/거짓입니다. 6은 별도 JavaScript 문장이 아닙니다. 넓은 화면과 좁은 화면 모두 N=6, E=7입니다.

이 예의 조건

  • 본문과 전체 도표의 예를 따라 첫 판정부터 FULL 반환까지 진행합니다.
관계도의 보충 설명
전체 도표의 표기·관계·조건
구분전체 도표의 내용
관계 설명노드 1은 status가 OPEN이 아닌지 판정한다. 참이면 3 CLOSED 반환, 거짓이면 2 정원 판정으로 이동한다. 노드 2는 참가 수가 정원 이상인지 판정한다. 참이면 4 FULL 반환, 거짓이면 5 AVAILABLE 반환으로 이동한다. 3,4,5 모두 개념적 종료 6에 합류한다. 여섯 노드와 일곱 변이다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
첫 판정

OPEN이 아닌지를 검사한다.

  • 분기 조건 1: status OPEN
  • 분기 조건 2: 노드 1=false
  • 노드 2로 이동
정원 판정

참가 수가 정원과 같다.

  • 분기 조건 1: count2>=capacity2=true
  • 노드 4로 이동
FULL 반환

반환하면 호출이 끝난다.

  • 분기 조건 1: return FULL
  • 분기 조건 2: 방문 1→2→4→6
  • 개념적 종료 6; 다른 return은 실행 안 함

조건 두 개가 반환 경로 세 개를 나눈다

OPEN이 아닌지를 검사한다. 반환하면 호출이 끝난다. 각 단계의 값과 전환 이유를 비교합니다.

SOFTWARE ENGINEERING · READING CLUB

조건 두 개가 반환 경로 세 개를 나눈다

숫자는 본문의 admission 코드와 대응합니다. 조건이 거짓일 때만 다음 조건으로 진행합니다.

조건 두 개가 반환 경로 세 개를 나눈다노드1 status가 OPEN이 아닌지 판정: 참이면3 CLOSED 반환, 거짓이면2 정원 판정. 노드2 참가수 이상 정원: 참이면4 FULL 반환, 거짓이면5 AVAILABLE 반환. 3,4,5 모두 개념적 종료6에 합류. 여섯 노드와 일곱 변. 1 status!== OPEN?2 count>= capacity?3 returnCLOSED4 returnFULL5 returnAVAILABLE6 종료개념적 합류TFTF
조건 두 개가 반환 경로 세 개를 나눈다노드1 status가 OPEN이 아닌지 판정: 참이면3 CLOSED 반환, 거짓이면2 정원 판정. 노드2 참가수 이상 정원: 참이면4 FULL 반환, 거짓이면5 AVAILABLE 반환. 3,4,5 모두 개념적 종료6에 합류. 여섯 노드와 일곱 변. 1 status!== OPEN?2 count>= capacity?3 returnCLOSED4 returnFULL5 returnAVAILABLE6 종료개념적 합류TFTF

마름모는 조건, 사각형은 반환, 더 둥근 종료 상자는 개념적 합류입니다. 화살표는 가능한 제어 이동, T/F는 조건의 참/거짓입니다. 6은 별도 JavaScript 문장이 아닙니다. 넓은 화면과 좁은 화면 모두 N=6, E=7입니다.

이 예의 조건

  • 본문과 전체 도표의 예를 따라 첫 판정부터 FULL 반환까지 진행합니다.

관계도의 보충 설명

전체 도표의 표기·관계·조건
구분전체 도표의 내용
관계 설명노드 1은 status가 OPEN이 아닌지 판정한다. 참이면 3 CLOSED 반환, 거짓이면 2 정원 판정으로 이동한다. 노드 2는 참가 수가 정원 이상인지 판정한다. 참이면 4 FULL 반환, 거짓이면 5 AVAILABLE 반환으로 이동한다. 3,4,5 모두 개념적 종료 6에 합류한다. 여섯 노드와 일곱 변이다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
첫 판정

OPEN이 아닌지를 검사한다.

  • 분기 조건 1: status OPEN
  • 분기 조건 2: 노드 1=false
  • 노드 2로 이동
정원 판정

참가 수가 정원과 같다.

  • 분기 조건 1: count2>=capacity2=true
  • 노드 4로 이동
FULL 반환

반환하면 호출이 끝난다.

  • 분기 조건 1: return FULL
  • 분기 조건 2: 방문 1→2→4→6
  • 개념적 종료 6; 다른 return은 실행 안 함

간선은 1→3, 1→2, 2→4, 2→5, 3→6, 4→6, 5→6의 일곱 개이고 노드는 여섯 개이므로 V = 7 - 6 + 2 = 3입니다. 판정이 두 개인 함수의 순환 복잡도가 판정의 수에 1을 더한 3인 것과도 맞습니다. 세 기본 경로와 그 경로를 지나는 입력은 다음과 같습니다.

입력지나는 노드건너뛴 판정결과
CLOSED, 참가자 0, 정원 21→3→6노드 2CLOSED
OPEN, 참가자 2, 정원 21→2→4→6없음FULL
OPEN, 참가자 1, 정원 21→2→5→6없음AVAILABLE

마감된 모임에서 참가자 수를 2로 바꾸어도 첫 return에서 끝나므로 새 경로가 생기지 않습니다. 세 경로는 각자 다른 두 경로에 없는 반환 간선을 하나씩 가지고 있어 서로 구별됩니다.

이 예제는 그래프의 모든 경로를 실제 입력으로 지날 수 있는 단순한 코드입니다. 실제 프로그램에는 조건끼리 모순되어 어떤 입력으로도 지날 수 없는 경로, 반복문, 예외가 있을 수 있으며, 반복 횟수까지 따지는 전체 경로와 여기서 고른 기본 경로는 다릅니다. 이 구분은 NIST SP500-235에 자세히 나옵니다. 순환 복잡도만큼 사례를 만들었다고 모든 입력과 예외, 요구를 확인한 것은 아니며, 순환 복잡도가 높다고 해서 결함이 있다고 단정할 수도 없습니다.

테스트 도구의 커버리지 보고를 읽는다

Node.js의 테스트 도구는 커버리지를 집계해 보고할 수 있습니다. 테스트 사례와 오라클에서 만든 core.test.mjs가 있는 reading-lab 폴더에서 다음 명령을 실행합니다. 이 옵션은 실험적 기능으로 표시되어 있어, 보고서의 모양과 집계 방식이 Node.js의 버전에 따라 달라질 수 있습니다.

node --experimental-test-coverage --test core.test.mjs

테스트 결과 아래에 core.mjs의 줄, 분기, 함수 커버리지의 비율과 실행되지 않은 줄의 번호가 나옵니다. 그 글의 두 테스트만 있다면 cancel, closeSession, restoreSession의 줄과 createSession이 오류를 던지는 줄이 실행되지 않은 줄로 나오므로, 어떤 테스트가 빠졌는지 찾는 데 쓸 수 있습니다.

다만 이 비율은 도구가 정한 방식으로 센 값입니다. 요구 R1부터 R6까지가 모두 알맞은 사례로 확인되었는지는 비기능 요구와 변경 추적의 추적 표와 따로 비교해야 합니다. 커버리지가 높아도 기대 결과가 틀린 테스트는 틀린 동작을 통과시킬 수 있습니다.

코드의 내부 구조를 보고 사례를 고르는 이런 방식을 화이트박스 테스트(white-box testing), 앞 글처럼 입력과 출력의 명세만 보고 고르는 방식을 블랙박스 테스트(black-box testing)라고 합니다. 두 방식은 서로를 보완하며, 다음 글에서 다룰 테스트 수준과는 다른 기준으로 나눈 것입니다.

정리

  • 커버리지는 정한 확인 대상 가운데 테스트가 실행하거나 만족한 것의 비율이므로, 무엇을 세는지 함께 말해야 합니다.
  • 분기 커버리지를 만족해도 단락 평가 때문에 어떤 조건은 실제로 참이나 거짓으로 평가되지 않았을 수 있습니다.
  • MC/DC는 조건 하나만 바꾼 사례를 기준 사례와 비교해, 각 조건이 결정을 독립적으로 바꾸는지 확인합니다.
  • 순환 복잡도 V = E - N + 2P는 기본 경로의 수를 알려 주며, 무엇을 노드와 간선으로 세었는지 함께 밝혀야 합니다.
  • 커버리지 보고는 빠진 테스트를 찾는 데 쓰고, 요구가 옳게 확인되었는지는 추적 표와 기대 결과로 따로 판단합니다.

연습 문제

1. 반복문이 있는 함수의 기본 경로

다음 함수의 제어 흐름 그래프를 그리고 순환 복잡도를 계산한 뒤, 기본 경로마다 입력을 하나씩 고르세요. for...of 반복은 원소가 남았는지 확인하는 판정 하나로 보고, 반복 안의 if와 함께 그래프에 넣으면 됩니다.

function countValid(ids) {
  let count = 0;
  for (const id of ids) {
    if (/^[a-z]/.test(id)) count += 1;
  }
  return count;
}
해설 보기

노드를 시작 1(count = 0), 반복할 원소가 남았는지 보는 판정 2, 첫 글자를 보는 판정 3, 개수를 더하는 4, 반환 5로 잡습니다. 간선은 1→2, 2→3, 2→5, 3→4, 3→2, 4→2의 여섯 개이므로 V = 6 - 5 + 2 = 3이며, 판정이 두 개라는 것과도 맞습니다. 기본 경로는 빈 배열로 바로 끝나는 1→2→5, 소문자로 시작하는 아이디 하나를 세는 1→2→3→4→2→5, 그렇지 않은 아이디 하나를 건너뛰는 1→2→3→2→5이고, 입력은 각각 [], ["min"], ["1a"]로 고를 수 있습니다. 이때 결과는 0, 1, 0입니다. 반복문이 있어 원소 하나마다 세는 길과 건너뛰는 길로 갈라지므로, 전체 경로의 수는 길이가 k인 배열만 따져도 2ᵏ개이고 배열이 길어질수록 끝없이 늘어나지만, 기본 경로는 세 개로 충분합니다.

2. MC/DC를 만족하는 사례

결정 status === "OPEN" && count < capacity에서 각 조건이 결정을 독립적으로 바꾼다는 것을 보이는 가장 적은 사례를 고르세요.

해설 보기

기준 사례로 두 조건이 모두 참인 ("OPEN", 1, 2)를 두면 결정은 참입니다. 첫 조건만 바꾼 ("CLOSED", 1, 2)와 둘째 조건만 바꾼 ("OPEN", 2, 2)는 모두 결정이 거짓이 되므로, 세 사례로 두 조건의 독립 효과를 보일 수 있습니다. 이때 ("CLOSED", 1, 2)에서는 단락 평가 때문에 둘째 조건이 실제로 평가되지 않는다는 점도 함께 기록해 둡니다.

3. 단언 없이 높아진 비율

어떤 테스트가 register를 여러 조건으로 부르기만 하고 결과는 전혀 단언하지 않습니다. 그 덕분에 커버리지가 크게 높아졌습니다. 이 수치가 알려 주는 것과 알려 주지 못하는 것을 구분하고, 개선 방법 두 가지를 적으세요.

해설 보기

이 수치는 집계 대상인 코드가 실행되었다는 것만 알려 줍니다. 거절 이유가 맞는지, 거절한 뒤 명단이 그대로인지, 중복이 막히는지는 전혀 확인하지 않았으므로 알 수 없습니다. 첫째로 각 호출에 요구에서 끌어낸 결과 코드와 상태의 단언을 더하고, 둘째로 중복 검사와 정원 검사의 순서를 바꾸는 결함을 일부러 넣어 테스트가 실패하는지 확인합니다. 커버리지는 높이는 것 자체가 목표가 아니라, 아직 시험하지 않은 곳을 찾는 데 쓰는 도구입니다.

다음 글 통합·인수·회귀 테스트에서는 함수 하나를 넘어, 여러 부분을 이어 붙였을 때와 사용자가 받아들일 때, 그리고 코드를 바꾼 뒤에 무엇을 시험하는지를 다룹니다.