22장: 테스트

22장 테스트

내용

  • 22.1 소프트웨어 품질에서 개발자 테스트의 역할
  • 22.2 개발자 테스트에 대한 바람직한 접근 방법
  • 22.3 여러 가지 교묘한 테스트 방법
  • 22.4 전형적인 오류
  • 22.5 테스트 지원 도구
  • 22.6 테스트를 향상시키는 방법
  • 22.7 테스트 기록을 보존하는 방법

관련 주제

  • 소프트웨어 품질: 20장
  • 협력적인 구현 방법: 21장
  • 디버깅: 23장
  • 통합: 29장
  • 구현을 위한 선행 조건: 3장

  • 테스트는 가장 보편적으로 이용하는 품질 개선 활동이다. 소프트웨어는 다양한 방법으로 테스트되며 어떤 것은 개발자가 수행하고 어떤 것은 전문 테스터가 수행한다.

  • 단위 테스트는 한 명의 개발자나 팀이 작성한 클래스나 루틴, 작은 프로그램을 실행하는 것으로, 완성된 시스템과는 별개로 테스트한다.
  • 컴포넌트 테스트는 여러 개발자나 개발팀이 참여하는 클래스, 패키지, 소형 프로그램, 다른 프로그램의 요소를 실행하는 것으로, 더 완전한 시스템과 별개로 테스트한다.
  • 통합 테스트는 여러 개발자나 개발팀이 만든 클래스나 패키지, 컴포넌트, 서브시스템을 두 개 이상 결합해 실행하는 것이다. 이와 같은 테스트는 전형적으로 테스트해야 하는 클래스가 두 개가 되는 순간 시작해서 전체 시스템 개발이 완료될 때까지 지속적으로 수행한다.
  • 회귀 테스트는 이전에 통과했던 테스트 집합을 가지고 소프트웨어에 있는 결함을 찾기 위해 이전에 실행했던 테스트 케이스를 반복하는 것이다.
  • 시스템 테스트는 다른 소프트웨어와 하드웨어 시스템과의 통합을 포함한 최종 환경에서 소프트웨어를 실행하는 것이다. 이 테스트는 보안과 성능, 자원 손실, 시간 문제, 저수준 통합에서는 테스트할 수 없는 문제를 테스트한다.

  • 이 장에서 “테스트”는 개발자가 하는 테스트를 가리킨다. 일반적으로 단위 테스트, 컴포넌트 테스트, 통합 테스트로 구성되지만, 때에 따라서 회귀 테스트와 시스템 테스트가 포함될 수도 있다. 추가로 베타 테스트, 고객 인수 테스트, 성능 테스트, 구성 테스트, 플랫폼 테스트, 스트레스 테스트, 사용성 테스트 등 전문 테스터가 수행하고 개발자는 거의 수행하지 않는 테스트도 매우 많다. 이러한 테스트는 이 장에서 더는 언급하지 않는다.

  • 테스트는 일반적으로 블랙박스(black-box) 테스트와 화이트박스(white-box) 테스트(글래스박스(glass-box) 테스트라고도 함)라는 두 가지 큰 범주로 나뉜다. “블랙박스 테스트”는 테스터가 테스트하는 항목의 내부 동작을 볼 수 없는 테스트를 가리킨다. 당연히 자신이 작성한 코드를 테스트할 때는 적용되지 않는다! “화이트박스 테스트”는 테스터가 테스트하는 항목의 내부 동작을 볼 수 있는 테스트를 가리킨다. 이러한 종류의 테스트는 개발자가 직접 작성한 코드를 테스트하는 데 사용한다. 블랙박스 테스트와 화이트박스 테스트 모두 장단점을 갖고 있다. 이 장에서는 개발자가 수행하는 테스트라서 화이트박스 테스트에 초점을 맞추고 있다.

  • 어떤 개발자들은 “테스트”와 “디버깅”을 구분 없이 사용하지만, 신중한 개발자는 두 활동을 구분한다. 테스트는 오류를 발견하기 위한 수단이다. 디버깅은 이미 발견된 오류의 원인을 진단하고 수정하는 수단이다. 이 장에서는 오로지 오류 발견에 관해서만 설명한다. 오류 수정은 23장 “디버깅”에 자세히 소개되어 있다.

  • 테스트에 대한 전체 주제는 구현 시 테스트 주제보다 훨씬 크다. 시스템 테스트, 스트레스 테스트, 블랙박스 테스트를 비롯해 테스트 전문가를 위한 그 밖의 주제는 이 장의 마지막에 있는 “참고 자료”에 소개되어 있다.

22.1 소프트웨어 품질에서 개발자 테스트의 역할

  • 테스트는 소프트웨어 품질 프로그램에서 중요한 부분이고 많은 경우에 유일한 부분이기도 하다.

  • 미국의 교육용 어린이 프로그램인 “세서미 스트리트”에 대한 소프트웨어 개발 활동 목록을 만들고 있는데 ‘이 중에서 어느 것이 다른 것들과 다른가요?’라고 묻는다면 그 답은 “테스트”가 될 것이다.

  • 테스트의 목표는 다른 개발 활동의 목표와 상반된다. 다른 모든 개발 활동의 목표는 오류를 예방하고 소프트웨어가 부서지지 않도록 하는 것이다.
  • 테스트가 오류가 없다는 것을 완벽하게 증명할 수는 없다. 오류가 없다는 것은 소프트웨어가 완벽하다는 의미일 수도 있지만, 그에 못지않게 테스트 케이스가 비효과적이고 불완전하다는 의미일 수도 있다.
  • 테스트 자체는 소프트웨어의 품질을 향상시키지 않는다. 소프트웨어를 향상시키고 싶다면 테스트를 더 많이 하지 말고 개발을 더 잘한다.
  • 테스트에는 코드에서 오류를 발견할 거라는 가정이 필요하다. 개발자들은 평균 15개 중에서 5개의 오류만 발견했다. 그러한 기대가 부자연스럽게 보이겠지만, 다른 사람이 아니라 자신이 오류를 찾기를 바라야 한다.

  • 가장 중요한 질문은 ‘일반적인 프로젝트에서 개발자 테스트에 얼마나 많은 시간을 보내야 하는가?’다.

  • 그림 22-1에서처럼 프로젝트의 크기와 복잡성에 따라 개발자 테스트는 전체 프로젝트 일정의 8%에서 25% 정도 차지한다. 이것은 다량의 보고된 데이터에서 변함이 없었다.

  • (그림 22-1: 프로젝트의 크기가 증가할수록 전체 개발 시간에서 개발자 테스트가 차지하는 비중이 조금씩 감소한다. 프로그램의 크기가 미치는 영향은 27장 “프로그램의 크기가 구현에 미치는 영향”에서 더 자세히 소개한다.)

  • 개발자 테스트 결과는 제품의 신뢰성을 평가하고, 결함 수정과 이후의 테스트·교육 방향을 정하는 데 사용한다.

구현 중의 테스트

  • 테스트라는 주제는 아주 광범위해서 이 장에서 다루는 “화이트박스 테스트”나 “글래스박스”는 때때로 무시한다.

  • 일반적으로 구현 중에 클래스나 루틴을 작성하고 머릿속에서 검사한 다음 코드를 검토하거나 테스트한다. 디버깅이 수월해진다.

  • 협력적인 구현 습관은 테스트가 제공할 수 없는 많은 것을 제공한다. 테스트의 기본 개념을 이해하면 더 나은 테스트를 할 수 있으며 테스트의 효과도 높일 수 있다.

22.2 개발자 테스트에 대한 바람직한 접근 방법

  • 개발자 테스트에 체계적으로 접근하면 최소한의 노력으로 모든 종류의 오류를 발견하는 능력을 극대화한다. 이 내용을 확실하게 이해하도록 한다.

  • 각 연관된 요구사항을 테스트해 요구사항이 구현되었는지 확인하라. 요구사항에서 자주 빠뜨리는 사항에 대한 테스트를 고려하라.
  • 각각 연관된 설계 사항이 구현되었는지를 보장하기 위해서 테스트하라. 설계 단계나 가능한 한 일찍(테스트할 루틴이나 클래스를 상세하게 작성하기 전) 이 단계의 테스트 케이스를 계획하라.
  • 요구사항과 설계를 테스트하는 테스트 케이스에 상세 테스트 케이스를 추가하는 데 “기초 테스트”를 사용하라. 최소한 코드의 모든 줄을 테스트해야 한다.
  • 현재 또는 이전 프로젝트에서 발견한 오류에 대한 체크리스트를 사용하라.

  • 제품과 함께 테스트 케이스를 설계한다. 결함을 빨리 고칠수록 비용이 저렴하기 때문에 가능한 한 빨리 테스트하고 발견할 수 있도록 계획을 세운다.

테스트를 먼저 할 것인가, 나중에 할 것인가?

  • 가끔 개발자들은 코드를 작성한 다음 테스트 케이스를 작성하는 것이 좋은지, 그 반대의 경우가 좋은지 고민한다(Beck 2003). 이것은 다음에 소개하는 테스트 케이스를 먼저 작성해야 하는 여러 가지 이유 중 하나다.

  • 코드를 작성하기 전에 테스트 케이스를 작성해도 코드를 작성한 후 테스트 케이스를 작성하는 것보다 더 많은 노력이 들지는 않는다. 이것은 단순히 테스트 케이스 작성 작업의 순서를 재배치하는 것이다.
  • 테스트 케이스를 먼저 작성하면 결함을 미리 발견하고 더 쉽게 수정할 수 있다.
  • 테스트 케이스를 먼저 작성하면 코드를 작성하기 전에 요구사항과 설계에 대해서 적어도 좀 더 생각하게 되며 그것이 더 좋은 코드를 만든다.
  • 테스트 케이스를 먼저 작성하면 코드가 작성되기 전에 요구사항에 있는 문제를 미리 노출한다. 요구사항이 잘못되어 있으면 테스트 케이스를 작성하기가 어렵기 때문이다.
  • 수행해야 하는 테스트 케이스를 저장해 놓으면 처음뿐만 아니라 나중에도 테스트할 수 있다.

  • 대체로 테스트 우선(test-first) 프로그래밍이 지난 수십 년 동안 나타난 소프트웨어 기법 중에서 가장 유용한 것 중의 하나라는 생각이 든다. 하지만 이 기법은 다음에 소개하는 개발자 테스트의 일반적인 한계를 갖기 때문에 테스트의 만병통치약은 아니다.

개발자 테스트의 한계

  • 다음과 같은 개발자 테스트의 한계를 주의한다.

  • 개발자 테스트는 “깨끗한 테스트”가 되기 쉽다. 미숙한 테스트 조직은 더러운 테스트를 한 번 할 때마다 깨끗한 테스트를 다섯 번 정도 하는 경향이 있다.

  • 개발자 테스트는 테스트 커버리지를 낙관적으로 바라보는 경향이 있다. 일반적인 개발자들이 스스로 95%의 테스트 커버리지를 달성한다고 믿지만, 전형적으로 최고 80%에서 최저 30%, 평균 50%에서 60% 정도의 테스트 커버리지를 달성한다(Boris Beizer in Johnson 1994).

  • 개발자는 단순한 명령문 커버리지를 충분하다고 보기 쉽지만, 조건마다 참과 거짓을 확인하는 분기 커버리지까지 점검해야 한다.

  • 이러한 사항 중 어느 것도 개발자 테스트의 가치를 떨어뜨리지는 않지만, 개발자 테스트에 대한 올바른 관점을 갖는 데 도움을 준다. 아무리 개발자 테스트가 훌륭하다고 하더라도 그것만으로는 적절한 품질 보증을 제공하기에 충분하지 않기 때문에 독립적인 테스트와 협력적인 구현 기법을 포함한 다른 기법으로 보완해야 한다.

22.3 여러 가지 교묘한 테스트 방법

  • 가능한 입력과 조합을 모두 시험할 수 없으므로 테스트만으로 프로그램의 정확성을 증명할 수 없다. 간단한 입력 프로그램도 경우의 수가 다음처럼 커진다.

  • 이름: $26^{20}$ (26가지 선택 가능성이 있는 20글자)
  • 주소: $26^{20}$ (26가지 선택 가능성이 있는 20글자)
  • 전화번호: $10^{10}$ (10가지 선택 가능성이 있는 10글자)
  • 가능한 총 경우의 수 = $26^{20} * 26^{20} * 10^{10} \approx 10^{66}$

  • 이처럼 비교적 입력이 적은데도 불구하고 $10^{66}$개나 되는 테스트 케이스가 있다. 더 현실적인 양의 데이터를 넣었다면 모든 경우의 수를 테스트하는 작업은 이보다 훨씬 더 불가능했을 거라는 점은 말할 필요도 없다.

불완전한 테스트

  • 현실적으로 말해 완전한 테스트는 불가능하므로 가장 오류를 잘 발견할 것 같은 테스트 케이스를 선택하는 것이 바로 테스트의 기술이다. $10^{66}$개의 가능한 테스트 케이스 중에서 몇 개의 테스트 케이스만이 다른 테스트 케이스가 드러내지 못하는 오류를 드러낼 것이다. 같은 결과를 반복해서 보여주는 것보다는 서로 다른 결과를 보여주는 몇 가지 테스트 케이스를 선택하는 데 집중해야 한다.

  • 테스트 계획을 수립할 때 새로운 것을 말해주지 않는 테스트는 제거한다.

구조적인 기초 테스트

  • 다소 엉성한 이름에도 불구하고 구조적인 기초 테스트는 꽤 단순한 개념이다. 모든 기본적인 사항을 다루게 하는 가장 간단한 방법은 프로그램의 실행 경로의 수를 계산하여 프로그램의 각 경로를 살펴볼 수 있는 최소한의 테스트 케이스를 개발하는 것이다.

  • “코드 커버리지” 테스트나 “논리 커버리지” 테스트에 대해 들어봤을 것이다. 코드 커버리지 테스트나 논리 커버리지 테스트를 사용하고 있다면 같은 논리 구조를 구조적인 기초 테스트로 다룰 때보다 훨씬 많은 테스트 케이스를 작성하게 될 것이다.

  • 다음과 같은 직관적인 방법으로 기초 테스트에 필요한 최소 케이스의 수를 계산할 수 있다.
    1. 루틴의 직선 경로에 대해 1로 시작한다.
    2. if, while, repeat, for, and, or 또는 이와 동등한 키워드에 대해서 1을 더한다.
    3. case 문에서 각 케이스마다 1을 더한다. case 문에 default 케이스가 없다면 1을 한 번 더 더한다.
  • 다음 예제를 살펴보자.
// 자바 프로그램을 통과하는 경로의 수를 계산하기 위한 간단한 예제
Statement1;         // <-- 루틴 자체를 "1"로 센다.
Statement2;
if ( x < 10 ) {     // <-- if 문을 "2"로 센다.
    Statement3;
}
Statement4;
  • 이 예제에서는 1로 시작하여 if 문을 한 번 세었기 때문에 총 2가 되었다. 이 예제에서는 다음과 같은 테스트 케이스를 만들어야 할 것이다.
  • (x < 10)일 때 실행되는 if에 의해서 제어되는 명령문
  • (x >= 10)일 때 실행되는 if에 의해서 제어되는 명령문

  • 이러한 종류의 테스트가 어떻게 작동하는지에 대한 정확한 개념을 제공하기 위해서 더 현실적인 예제가 필요하다. 이 경우에는 현실성을 위해서 결함이 있는 코드를 포함하고 있다.

  • 다음 소스코드는 약간 더 복잡한 예제다. 이 코드는 이 장 전체에 걸쳐서 사용되며 몇 가지 오류를 포함하고 있다.
// 자바 프로그램의 기초 테스트에 필요한 케이스의 수를 계산하기 위한 예제
1  // 세금공제 급여를 계산한다.
2  totalWithholdings = 0;
3  
4  for ( id = 0; id < numEmployees; id++ ) {  // <-- for 문에 대해서 "2"를 센다.
5      // 최댓값보다 작을 경우 사회보장연금을 계산한다.
6      if ( m_employee[ id ].governmentRetirementWithheld < MAX_GOVT_RETIREMENT ) { // <-- if 문에 대해서 "3"을 센다.
7          governmentRetirement = ComputeGovernmentRetirement( m_employee[ id ] );
8      }
9 
10     // 퇴직금이 없는 것을 기본값으로 설정한다.
11     companyRetirement = 0;
12 
13     // 임의의 퇴직금을 결정한다.
14     if ( m_employee[ id ].WantsRetirement &&        // <-- if 문에 대해서 "4"를 세고 &&에 대해서 "5"를 센다.
15          EligibleForRetirement( m_employee[ id ] ) ) {
16         companyRetirement = GetRetirement( m_employee[ id ] );
17     }
18 
19     grossPay = ComputeGrossPay ( m_employee[ id ] );
20 
21     // 퇴직 연금을 결정한다.
22     personalRetirement = 0;
23     if ( EligibleForPersonalRetirement( m_employee[ id ] ) ) {  // <-- if 문에 대해서 "6"을 센다.
24         personalRetirement = PersonalRetirementContribution( m_employee[ id ],
25             companyRetirement, grossPay );
26     }
27 
28     // 주급을 계산한다.
29     withholding = ComputeWithholding( m_employee[ id ] );
30     netPay = grossPay - withholding - companyRetirement - governmentRetirement -
31         personalRetirement;
32     PayEmployee( m_employee[ id ], netPay );
33 
34     // 이 직원의 급여를 전체 급여에 더한다.
35     totalWithholdings = totalWithholdings + withholding;
36     totalGovernmentRetirement = totalGovernmentRetirement + governmentRetirement;
37     totalRetirement = totalRetirement + companyRetirement;
38 }
39 
40 SavePayRecords( totalWithholdings, totalGovernmentRetirement, totalRetirement );
  • 이 예제에서는 첫 번째 테스트 케이스 하나와 다섯 개의 키워드에 대해 각각 1을 더해 총 여섯 개의 테스트 케이스가 필요할 것이다.

  • 다음은 이 예제의 모든 기본적인 사항을 다루는 테스트 케이스다.

케이스 테스트 설명 테스트 데이터
1 명목적인 케이스 모든 불린 조건이 참이다.
2 초기 for 조건이 거짓이다. numEmployees < 1
3 첫 번째 if가 거짓이다. m_employee[ id ].governmentRetirementWithheld >= MAX_GOVT_RETIREMENT
4 첫 번째 and 부분이 거짓이기 때문에 두 번째 if가 거짓이다. not m_employee[ id ].WantsRetirement
5 두 번째 and 부분이 거짓이기 때문에 두 번째 if가 거짓이다. not EligibleForRetirement( m_employee[id] )
6 세 번째 if가 거짓이다. not EligibleForPersonalRetirement( m_employee[ id ] )

참고: 이 표는 이 장 전체에서 추가적인 테스트 케이스로 확장될 것이다.

  • 루틴이 이 예제보다 훨씬 복잡했다면 모든 경로를 다루기 위해 사용해야 하는 테스트 케이스의 수는 급격하게 증가할 것이다.

  • 여섯 개의 테스트 케이스로 모든 코드 경로를 실행해도 데이터의 변화까지 검증한 것은 아니므로 테스트가 완전하다고 할 수 없다.

데이터 흐름 테스트

  • 마지막 절과 이 절에서는 제어 흐름과 데이터 흐름이 모두 컴퓨터 프로그래밍에서 동등하게 중요하다는 것을 설명하는 또 다른 예제를 보여줄 것이다.

  • 데이터 흐름 테스트는 데이터 사용이 적어도 제어 흐름만큼 오류를 유발할 수 있다는 개념에 기반을 두고 있다. 보리스 바이저는 모든 코드는 절반 이상이 데이터 선언과 초기화로 이루어져 있다고 주장했다(Beizer 1990).

  • 데이터는 다음 세 가지 상태 중 한 가지로 존재할 수 있다.
  • 정의 데이터가 초기화되었지만, 아직 사용되지 않았다.
  • 사용 데이터가 루틴의 인자로서 계산되거나 다른 무언가를 위해서 사용되었다.
  • 삭제 데이터가 정의되었지만 어떤 방식으로 정의가 해제되었다. 가령 데이터가 포인터라면 아마도 포인터가 해제되었을 것이다.

  • “정의”, “사용”, “삭제”라는 용어뿐만 아니라 변수에 무언가를 하기 바로 전후에 루틴에 들어가거나 루틴에서 빠져나오는 것을 설명하는 용어가 있으면 편리하다.
  • 들어감 제어 흐름이 변수가 사용되기 바로 전에 루틴에 들어간다. 예를 들면 작업 변수가 루틴의 앞부분에서 초기화된다.
  • 빠져나옴 제어 흐름이 변수가 사용되고 나서 바로 다음에 루틴을 떠난다. 예를 들면 리턴 값이 루틴의 끝에서 상태 변수에 할당된다.

  • 데이터 상태의 조합
  • 일반적인 데이터 상태의 조합은 변수가 정의되고 한 번 이상 사용된 다음, 때에 따라 삭제되는 것이다. 다음 패턴을 의심의 눈초리로 살펴본다.
  • 정의-정의 값이 유지되기 전에 한 변수를 두 번 정의해야 한다면 더 나은 프로그램이 아니라 더 좋은 컴퓨터가 필요하다! 이것은 실제로 잘못되지 않았더라도 불필요하고 오류를 유발할 가능성이 있다.
  • 정의-빠져나옴 변수가 지역 변수라면 정의한 다음 사용하지 않고 빠져나오는 것은 이치에 맞지 않는다. 루틴 매개변수나 전역 변수라면 문제는 없을 것이다.
  • 정의-삭제 변수를 정의한 다음 삭제한다는 것은 변수가 불필요하거나 변수를 사용하기로 되어 있던 코드가 빠져있다는 것을 의미한다.
  • 들어감-삭제 이러한 경우는 해당 변수가 지역 변수일 때는 문제가 된다. 정의되거나 사용되지 않았다면 삭제될 필요가 없을 것이다.
  • 들어감-사용 이번에도 해당 변수가 지역 변수일 때는 문제가 된다. 변수는 사용되기 전에 정의되어야 한다.
  • 삭제-삭제 변수가 두 번 삭제되어서는 안 된다. 변수는 되살아나지 않는다. 컴퓨터를 멈추게 하는 가장 좋은 방법이 포인터를 두 번 삭제(해제)하는 것이다.
  • 삭제-사용 삭제된 변수를 사용하는 것은 논리적인 오류다. 그 코드가 어쨌든 작동하는 것처럼 보인다고 하더라도(예: 해제된 메모리를 계속해서 가리키고 있는 포인터) 우연일 뿐이며 머피의 법칙대로 해당 코드는 가장 혼란스러운 순간에 작동을 멈출 것이다.
  • 사용-정의 변수를 사용하고 나서 정의하는 것은 해당 변수가 사용되기 전에 정의되었는지에 따라서 문제가 될 수도 있고 되지 않을 수도 있다. 하지만 분명한 점은 사용-정의 패턴을 보면 앞에서 정의되었는지를 검사할 필요가 있다는 것이다.

  • 테스트를 시작하기 전에 이러한 데이터 상태의 모순된 순서를 검사한다. 이 작업은 다음을 포함하여 다양한 수준으로 완전하게 수행할 수 있다.
  • 모든 정의 모든 변수의 모든 정의를 테스트한다. 즉, 변수가 값을 받는 모든 곳에서 테스트한다. 모든 소스코드를 조사하려고 하면 이 작업을 기본적으로 수행해야 하므로 이는 강력한 전략이 아니다.
  • 모든 정의-사용 조합 변수를 정의하고 다른 곳에서 사용하는 모든 조합을 테스트한다. 이는 모든 코드를 실행하는 것만으로는 모든 정의-사용 조합이 테스트된다는 것을 보장하지 못하기 때문에 모든 정의를 테스트하는 것보다 강력한 전략이다.

  • 다음 예제를 살펴보자.
// 데이터 흐름이 테스트되는 프로그램을 자바로 작성한 예제
if ( Condition 1 ) {
    x = a;
}
else {
    x = b;
}
if ( Condition 2 ) {
    y = x + 1;
}
else {
    y = x - 1;
}
  • 이 프로그램에서 모든 경로를 다루기 위해서는 Condition 1이 참인 테스트 케이스와 거짓인 테스트 케이스가 필요하다.

  • 하지만 모든 정의-사용 조합을 다루기 위해서는 몇 가지 케이스가 더 필요하다. 지금은 Condition 1Condition 2가 동시에 참이고 거짓인 케이스를 생성한 상태다.

x = a
...
y = x + 1
  • x = b
    ...
    y = x - 1
    
  • 하지만 모든 정의-사용 조합을 테스트하기 위해서는 (1) $x = a$이고 $y = x - 1$과 (2) $x = b$이고 $y = x + 1$이라는 두 개의 케이스가 더 필요하다. 이 예제에서는 케이스 3(Condition 1=True, Condition 2=False)과 케이스 4(Condition 1=False, Condition 2=True)라는 두 개의 케이스를 추가해 이러한 조합을 얻을 수 있다.

  • 테스트 케이스를 개발하는 좋은 방법은 모든 정의-사용 데이터 흐름은 아니더라도 일부는 제공하는 구조적인 기초 테스트로 시작하는 것이다. 그러고 나서 정의-사용 데이터 흐름 테스트 케이스를 완성하는 데 필요한 케이스를 추가한다.

  • 앞 절에서 설명했듯이 구조적인 기초 테스트는 “자바 프로그램의 기초 테스트에 필요한 케이스의 수를 계산하기 위한 예제”에서 루틴을 시작할 때 여섯 개의 테스트 케이스를 제공했다.
케이스 테스트 설명
7 12번째 줄에 companyRetirement를 정의하고 26번째 줄에서 처음으로 사용한다.
이전의 테스트 케이스에 의해서 다루어질 필요는 없다.
8 15번째 줄에 companyRetirement를 정의하고 31번째 줄에서 처음으로 사용한다.
이전의 테스트 케이스에 의해서 다루어질 필요는 없다.
9 17번째 줄에 companyRetirement를 정의하고 31번째 줄에서 처음으로 사용한다.
이전의 테스트 케이스에 의해서 다루어질 필요는 없다.
  • 일단 데이터 흐름 테스트 케이스 목록을 처리하는 과정을 몇 번 수행해 보면 어떤 케이스가 효과적이고 어떤 케이스를 이미 다루었는지 알 수 있을 것이다.

등가 분할

  • 좋은 테스트 케이스는 가능한 입력 데이터의 넓은 부분을 다룬다. 두 테스트 케이스가 정확하게 같은 오류를 제거한다면 그중 하나만 필요하다.

  • 앞에서 살펴본 “자바 프로그램의 기초 테스트에 필요한 케이스의 수를 계산하기 위한 예제”에서 7번째 줄이 등가 분할을 사용하기에 좋은 위치다. 이 케이스는 두 개의 동등한 케이스를 갖는다.

  • 기초 테스트와 데이터 흐름 테스트를 사용하여 이미 프로그램을 다루었다면 등가 분할에 대해서 생각한다고 하더라도 프로그램에 대해 많은 것을 알아낼 수는 없을 것이다. 하지만 외부(소스코드가 아닌 명세서를 통하여)에서 프로그램을 살펴보고 있거나 데이터가 복잡하고 데이터의 조합이 프로그램의 논리에 모두 반영되어 있지 않을 때는 특히 유용하다.

오류 추측

  • 좋은 개발자는 형식적인 테스트 기법과 더불어 덜 형식적이고 경험적인 여러 가지 기법을 사용해 자신의 코드에 있는 오류를 들춰낸다.

  • 직관이나 과거의 경험에 근거하여 추측할 수 있다. 21장 “협력 구현”에서는 정밀 검토의 한 가지 장점이 일반적으로 많이 발생하는 오류의 목록을 만들고 유지할 수 있다는 점이라고 지적한다. 그 목록은 새로운 코드를 검사하는 데 사용된다.

  • 이어지는 절에서는 오류 추측에 적합한 특정한 오류의 종류에 관해서 설명한다.

경계 분석
  • 테스트가 가장 유용한 영역 중 하나는 경계 조건, 즉 하나 차이로 인한 오류다. num을 써야 할 때 num - 1을 쓰고, >을 써야 할 때 >=라고 쓰는 것이 가장 흔한 실수다.

  • 경계 분석의 개념은 경계 조건을 조사하는 테스트 케이스를 작성하는 것이다. 다음 그림과 같이 max보다 작은 값의 범위를 테스트하고 있다면 세 가지 조건을 가진 것이다.

  • (그림 생략: Max보다 작은 경계, Max, Max보다 큰 경계)

  • 그림과 같이 max보다 작을 때, max일 때, max보다 클 때의 세 가지 경계 조건이 있다. 어느 조건도 실수하지 않았다는 것을 보장하기 위해서 세 가지 경우를 테스트해야 한다.

  • 앞에서 살펴본 코드 예제에는 m_employee[ ID ].governmentRetirementWithheld < MAX_GOVT_RETIREMENT에 대한 테스트가 들어 있다. 경계 분석 원칙에 따라 세 가지 케이스를 조사해야 한다.

케이스 테스트 설명
1 케이스 1은 m_employee[ ID ].governmentRetirementWithheld < MAX_GOVT_RETIREMENT의 참인 조건이 경계가 참이라는 측면에서 첫 번째 케이스가 되도록 정의되었다. 따라서 Case 1 테스트 케이스는 m_employee[ ID ].governmentRetirementWithheldMAX_GOVT_RETIREMENT - 1로 설정한다. 이 테스트 케이스는 이미 만들어졌다.
3 케이스 3은 m_employee[ ID ].governmentRetirementWithheld < MAX_GOVT_RETIREMENT의 거짓인 조건이 경계가 거짓이라는 측면에 있도록 정의되었다. 따라서 Case 3 테스트 케이스는 m_employee[ ID ].governmentRetirementWithheldMAX_GOVT_RETIREMENT + 1로 설정한다. 이 테스트 케이스도 이미 만들어졌다.
10 추가적인 테스트 케이스가 m_employee[ ID ].governmentRetirementWithheld = MAX_GOVT_RETIREMENT가 경계인 경우에 대해서 추가되었다.
복합 경계
  • 경계 분석은 허용 가능한 최댓값과 최솟값에도 적용된다. 이 예제에서는 grossPaycompanyRetirement, PersonalRetirementContribution의 최댓값/최솟값이겠지만 그 값을 계산하는 것은 루틴의 범위를 벗어나기 때문에 그에 대한 테스트 케이스는 더는 여기서 언급하지 않는다.

  • 더 미묘한 경계 조건은 경계에 변수가 조합될 때 발생한다. 가령 두 변수를 곱하는데 두 값 모두 큰 양수거나 큰 음수거나 0이라면 어떻게 될까? 루틴에 전달되는 모든 문자열이 비정상적으로 길다면 또 어떻게 될까?

  • 이 예제에서 규모가 큰 직원 그룹에 있는 멤버가 월급을 많이 받는다면(개발자 그룹의 멤버가 각각 25만 달러를 받는다고 하자) totalWithholdings, totalGovernmentRetirement, totalRetirement 변수에 무슨 일이 일어나는지 알고 싶을 것이다. 이것은 또 다른 테스트 케이스를 요구한다.

케이스 테스트 설명
11 각 멤버가 많은 월급(개발되고 있는 구체적인 시스템에 따라서 얼마나 “많은”지가 정해진다)을 받는 규모가 큰 직원 그룹. 예를 들어 월급이 25만 달러면서 사회보장 세금은 원천 징수하지 않고 퇴직금 적립은 하고 싶어 하는 1,000명의 직원이 있을 것이다.
  • 같은 맥락이지만 반대 경우에 대한 테스트 케이스는 월급이 $0.00인 멤버를 포함하는 작은 직원 그룹이 될 것이다.
케이스 테스트 설명
12 월급이 $0.00인 10명의 직원 그룹
나쁜 데이터
  • 경계 조건에서 발생하는 오류를 추측하는 것 이외에도 여러 가지 나쁜 데이터의 종류에 대해서 추측하고 테스트할 수 있다. 다음은 전형적인 나쁜 데이터에 대한 테스트 케이스다.

  • 너무 적은 데이터(또는 데이터가 전혀 없다).
  • 너무 많은 데이터
  • 틀린 종류의 데이터(유효하지 않은 데이터)
  • 잘못된 크기의 데이터
  • 초기화되지 않은 데이터

  • 이러한 제안들에 따라서 생각해낼 수 있는 테스트 케이스들 중 일부는 이미 다루었다.
케이스 테스트 설명
13 직원이 1억 명인 배열. 너무 많은 데이터를 테스트한다. 물론 너무 많은 게 얼마인지는 시스템에 따라서 다르겠지만, 예를 들기 위해 이것이 너무 많다고 가정하자.
14 음수인 월급. 틀린 종류의 데이터.
15 음수인 직원의 수. 틀린 종류의 데이터.
좋은 데이터
  • 프로그램에 있는 오류를 찾으려고 할 때 정상적인 경우가 오류를 포함할 수 있다는 사실을 간과하기가 쉽다. 다음은 그 외에 검사해 볼 만한 좋은 데이터의 종류다.

  • 명목상 케이스, 일반적이고 예상된 값
  • 최소한의 정상적인 구성
  • 최대한의 정상적인 구성
  • 이전 데이터와의 호환

  • 최소한의 정상적인 구성은 단순히 한 항목뿐만 아니라 항목 집합을 테스트할 때도 유용하다. 예를 들면 스프레드시트를 테스트할 때 빈 스프레드시트를 저장하는 경우다.
케이스 테스트 설명
16 직원이 한 명인 그룹. 최소한의 정상적인 구성을 테스트하기 위한 것이다.

22.5 테스트 지원 도구

  • 그래서 프로그램의 암호화 부분과 복호화 부분을 완전하게 조사하는 테스트 데이터 생성기를 구축했다.
  • 파일의 평균 길이가 30K가 되도록 테스트 케이스의 가중치를 두었는데, 이는 최대 길이인 500K보다 훨씬 짧았다.
  • 결과는 만족스러웠다. 약 100번의 테스트 케이스를 실행한 후에 프로그램에 있는 두 개의 오류를 발견했다. 테스트했던 파일의 내용, 길이, 암호의 범위 내에서는 프로그램이 정확하다는 것을 확실히 보장할 수 있었다.

  • 다음은 이 경험에서 얻은 몇 가지 교훈이다.

  • 적절하게 설계된 임의의 데이터 생성기는 생각지 못한 특이한 테스트 데이터의 조합을 생성할 수 있다.
  • 임의의 데이터 생성기는 개발자가 할 수 있는 것보다 훨씬 철저하게 프로그램을 조사할 수 있다.
  • 임의로 생성된 테스트 케이스가 현실적인 입력의 범위를 강조할 수 있도록 개량할 수 있다. 그렇게 하면 사용자에 의해서 사용될 가능성이 높은 영역에서의 테스트에 집중하여 해당 영역에서의 신뢰성을 최대화한다.
  • 테스트하는 코드가 변경된다면 테스트 드라이버를 재사용할 수 있다. 앞에서 소개한 사례에서 초기에 발견한 두 오류를 수정하고 나서 곧바로 테스트를 다시 시작할 수 있었다.

커버리지 모니터

  • 칼 위거스는 코드 커버리지를 측정하지 않고 테스트가 수행되면 전형적으로 코드의 50%에서 60% 정도만 조사한다고 보고했다(Wiegers 2002). 커버리지 모니터는 테스트 케이스가 코드를 완전하게 조사하는지를 말해주기 때문에 체계적인 테스트에 특히 유용하다.

데이터 기록/로깅

  • 어떤 도구는 프로그램을 감시해 실패 이벤트가 발생할 때 프로그램의 상태에 대한 정보를 수집할 수 있다.

  • 중요한 이벤트를 파일에 기록함으로써 자신만의 데이터 레코더를 구축할 수 있다. 그렇지 않고 스스로 걸러내는 저장소와 오류 메시지의 배치와 내용에 대해서 신중하게 고려하여 로깅을 구현한다면 릴리스 버전에도 로깅 기능을 포함시킬 수 있다.

심볼릭 디버거

  • 심볼릭 디버거는 코드를 검토하고 정밀 검사하기 위한 기술적인 도구다. 디버거에서 코드를 한 단계씩 실행시키고 작동하는 것을 보는 과정은 매우 유용하다.

  • 디버거에서 코드를 검토하는 것은 코드를 다른 개발자가 한 단계씩 살펴보면서 검토하는 것과 여러 가지 면에서 비슷한 절차다. 다양한 입력 데이터를 이용하여 코드가 작동하는 것을 보면 자신이 의도한 대로 코드를 구현했다는 것을 확신할 수 있다.

  • 좋은 디버거는 코드가 어떻게 실행되는지 정확하게 볼 수 있어 언어에 대해서 배울 수 있는 좋은 도구다.

시스템 교란기

  • 다른 테스트 지원 도구는 시스템을 교란하기 위한 것이다.

  • 이러한 종류의 테스트 지원 도구는 다음과 같은 다양한 기능을 제공한다.

  • 메모리 채우기 초기화되지 않은 변수가 없다는 것을 확인하고 싶을 것이다. 어떤 경우에는 메모리가 특정한 값으로 채워질 수도 있다.
  • 메모리 섞기 다중 작업 시스템에서는 어떤 도구가 프로그램이 작동할 때 메모리를 재배치하여 상대적인 위치가 아닌 절대적인 위치에 있는 데이터에 의존하는 코드가 작성되지 않게 해준다.
  • 선택적 메모리 실패 메모리 드라이버는 메모리가 부족하여 메모리 요청에 실패하거나 실패하기 전에 임의의 횟수만큼 메모리 요청을 허용하거나 메모리 요청을 한 번 허용하기 전에 임의의 횟수만큼 요청에 실패하도록 메모리가 부족한 상황을 흉내 낼 수 있다. 이러한 기능은 동적으로 할당된 메모리를 가지고 작업하는 복잡한 프로그램을 테스트할 때 특히 유용하다.
  • 메모리 접근 검사(경계 검사) 경계 검사는 포인터가 제대로 작동하도록 보장하기 위해서 포인터 연산을 감시한다. 이러한 도구는 초기화되지 않거나 허상 포인터를 감지하는 데 유용하다.

오류 데이터베이스

  • 가장 강력한 테스트 도구 중 하나는 보고된 오류의 데이터베이스다. 그런 데이터베이스는 관리 도구이면서 기술적인 도구이기도 하다.

22.6 테스트를 향상시키는 방법

  • 테스트를 향상시키는 단계는 다른 프로세스를 개선하는 단계와 유사하다. 다음 절에서는 이러한 방법을 테스트에 어떻게 적용할 거인지 설명한다.

테스트 계획 세우기

  • 효과적인 테스트를 수행하기 위한 핵심적인 요소 중 하나는 프로젝트의 시작부터 테스트에 대한 계획을 세우는 것이다.

다시 테스트하기(회귀 테스트)

  • 제품을 철저하게 테스트했는데 오류를 하나도 발견하지 못했다고 하자. 테스트는 소설계되고 이를 “회귀 테스트”라고 부른다.

  • 변경한 후에 체계적으로 변경된 사항을 다시 테스트할 수 없다면 고급 소프트웨어 제품을 생산하기란 거의 불가능하다.

자동 테스트

  • 회귀 테스트를 관리하는 유일한 현실적인 방법은 테스트를 자동화하는 것이다. 결국, 오류를 못 보고 지나치기가 쉬워져서 회귀 테스트의 목적이 무산된다.

  • 테스트 자동화의 이점은 다음과 같다.

  • 자동 테스트는 수동 테스트보다 잘못될 확률이 낮다.
  • 일단 테스트를 자동화하면 거의 아무런 노력을 들이지 않고 프로젝트의 나머지 부분에서 사용할 수 있다.
  • 테스트가 자동화되면 코드를 체크인할 때 기존 코드에 어떤 영향을 주는지를 살펴보기 위해 자주 실행할 수 있다. 테스트 자동화는 일일 빌드와 스모크(smoke) 테스트, 익스트림 프로그래밍과 같이 테스트 집약적인 기법의 바탕을 이룬다.
  • 자동 테스트는 주어진 문제를 가능한 한 초기에 발견할 수 있도록 해주며 이는 문제의 원인을 규명하고 수정하는 데 필요한 작업을 최소화하는 경향이 있다.
  • 자동 테스트는 수정 중에 입력된 결함을 빠르게 발견할 가능성을 높여주기 때문에 큰 규모의 변경을 위한 안전망을 제공한다.
  • 자동 테스트는 새롭고 변하기 쉬운 기술 환경에서 특히 유용하다. 그러한 환경에 있는 변경 사항을 초기에 발견하기 때문이다.

  • 자동 테스트를 지원하기 위해서 사용되는 주요 도구는 테스트 비계를 제공하고 입력을 생성하고 출력을 가로채고 예상 출력과 실제 출력을 비교한다. 앞 절에서 소개한 다양한 도구가 이러한 기능 중 일부나 전부를 수행할 것이다.

22.7 테스트 기록을 보존하는 방법

  • 테스트 프로세스를 반복할 수 있도록 만드는 것 외에도 변경 사항이 프로젝트를 향상시켰는지 또는 손상시켰는지 확실하게 알 수 있도록 프로젝트를 측정해야 한다. 다음은 프로젝트를 측정하기 위해 수집할 수 있는 몇 가지 데이터 종류다.

  • 결함에 대한 관리상의 설명(보고된 날짜, 보고한 사람, 제목 또는 설명, 빌드 번호, 수정된 날짜)
  • 문제에 대한 자세한 설명
  • 문제를 반복하기 위해 거치는 단계
  • 문제에 대해 제안된 해결책
  • 관련된 결함
  • 문제의 심각성(예: 치명적 문제, 귀찮은 문제, 외관상 문제 등)
  • 결함의 원인: 요구사항, 설계, 코드 작성, 테스트
  • 코드 작성 결함의 하위 분류: 하나 차이로 인한 오류, 잘못된 할당, 잘못된 배열 인덱스, 잘못된 루틴 호출 등
  • 수정 때문에 변경된 클래스와 루틴
  • 결함에 의해서 영향을 받는 코드의 줄 번호
  • 결함을 찾는 데 걸린 시간
  • 결함을 수정하는 데 걸린 시간

  • 일단 이러한 데이터를 수집하면 프로젝트가 손상되었는지 향상되었는지 결정할 수 있는 몇 가지 수치를 계산할 수 있다.

  • 나쁜 클래스로 시작해서 좋은 클래스 순으로 정렬되어 있고 가능한 한 클래스의 크기에 따라서 표준화되어 있는 각 클래스에 있는 결함의 수
  • 나쁜 루틴으로 시작해서 좋은 루틴 순으로 정렬되어 있고 가능한 한 루틴의 크기에 따라서 표준화되어 있는 각 루틴에 있는 결함의 수
  • 결함을 발견하는 데 걸린 평균 테스트 시간
  • 테스트 케이스당 발견된 평균 결함의 수
  • 결함을 수정하는 데 걸린 평균 프로그래밍 시간
  • 테스트 케이스에 의해서 테스트된 코드의 비율
  • 심각성의 정도별로 해결되지 않은 결함의 수

개인 테스트 기록

  • 프로젝트 수준의 테스트 기록뿐만 아니라 개인별로 테스트 기록을 유지하는 것이 유용하다는 것을 알게 될 것이다. 그러한 기록에는 가장 흔히 실수하는 오류에 대한 목록뿐만 아니라 코드를 작성하고 테스트하고 오류를 수정하는 데 걸리는 시간이 들어갈 수 있다.

참고 자료

  • 이 책에서 다루고 있는 것보다 테스트에 대해 더 깊이 있게 다루는 다른 책을 소개하고자 한다. 또한 개발자 테스트 주제에 대해 더 깊이 소개한다.

테스트

  • 셈 카너(Cem Kaner), 잭 포크(Jack Falk), 홍 Q(Hung Q). 이 책의 내용은 규모가 큰 웹사이트와 패키지화된 상품과 같이 널리 퍼져 있는 고객들에게 배포된 응용 프로그램을 테스트하는 데 가장 적합하지만, 일반적인 분야에서도 유용하다.
  • 셈 카너, 제임스 바흐(James Bach), 브렛 페티코드(Bret Pettichord) 《Lessons Learned in Software Testing》(John Wiley & Sons, 2002).
  • 루이스 탐르(Louise Tamre) 《Introducing Software Testing》(Addison-Wesley, 2002).
  • 제임스 휘테이커(James Whittaker) 《How to Break Software: A Practical Guide to Testing》(Addison-Wesley, 2002).
  • 제임스 휘테이커, “What Is Software Testing?”
  • 글렌포드 마이어스 《소프트웨어 테스팅의 정석》(에이콘출판사, 2012).

테스트 비계

  • 존 벤틀리 《생각하는 프로그래밍》(인사이트, 2014)의 “프로그래밍에서의 사소한 문제”. 이 글은 테스트 비계에 대한 여러 가지 훌륭한 예제를 포함하고 있다.
  • 팀 맥키넌, 스티브 프리만, 필립 크레이그 “Endo-Testing: Unit Testing with Mock Objects”(eXtreme Programming and Flexible Processes Software Engineering - XP2000” 컨퍼런스, 2000). 이 글은 개발자 테스트를 지원하기 위한 목 객체의 사용을 설명하고 있는 논문이다.
  • 데이브 토마스, 앤디 헌트, “Mock Objects”(IEEE Software, 2002년 5월/6월). 이 글은 개발자 테스트를 지원하기 위한 목 객체의 사용에 대해서 쉽게 읽을 수 있는 소개 글이다.
  • www.junit.org 이 사이트는 JUnit을 사용하여 개발자 테스트에 대한 지원을 제공한다. 유사한 리소스가 cppunit.sourceforge.net과 nunit.sourceforge.net에서 제공된다.

테스트 주도 개발

  • 켄트 벡 《테스트 주도 개발》(인사이트, 2014). 이 책은 실제 코드로 작성된 포괄적인 예제를 갖고 있다.

관련 표준

  • IEEE Std 1008-1987 (R1993), Standard for Software Unit Testing
  • IEEE Std 829-1998, Standard for Software Test Documentation
  • IEEE Std 730-2002, Standard for Software Quality Assurance Plans

체크리스트: 테스트 케이스

  • 클래스나 루틴에 적용되는 각 요구사항이 고유한 테스트 케이스를 갖고 있는가?
  • 클래스나 루틴에 적용되는 각 설계 요소가 고유한 테스트 케이스를 갖고 있는가?
  • 모든 코드를 적어도 하나의 테스트 케이스로 테스트했는가? 모든 코드를 조사하는 데 필요한 최소화된 테스트의 수를 계산하여 이를 검증했는가?
  • 모든 정의-사용 데이터 흐름 경로를 적어도 하나의 테스트 케이스로 테스트했는가?
  • 정의-정의, 정의-빠져나감, 정의-삭제와 같이 정상적으로 보이지 않는 데이터 흐름 패턴에 대해서 코드를 검사했는가?
  • 과거에 빈번하게 발생했던 오류를 발견하기 위한 테스트 케이스를 작성하는 데 일반적인 오류 목록을 사용했는가?
  • 모든 단순한 경계(최대, 최소, 하나 차이의 경계)를 테스트했는가?
  • 복합 경계 즉, 계산된 결과가 너무 작거나 너무 클 수 있는 입력 데이터의 조합을 테스트했는가?
  • 테스트 케이스가 잘못된 종류의 데이터에 대해서 검사했는가? 예를 들면 급여 프로그램에서 직원의 수가 음수인 경우.
  • 대표적이고 중간 정도에 해당하는 값을 테스트했는가?
  • 최대한의 정상적인 구성을 테스트했는가?
  • 최소한의 정상적인 구성을 테스트했는가?
  • 이전 데이터와 호환성을 테스트했는가? 그리고 이전 하드웨어, 운영체제의 이전 버전, 다른 소프트웨어의 이전 버전과의 인터페이스를 테스트했는가?
  • 테스트 케이스가 수동 검사에 도움이 되는가?

요점 정리

  • 개발자에 의한 테스트는 완전한 테스트 전략을 위한 핵심적인 부분이다. 독립적인 테스트도 중요하지만, 그 부분은 이 책의 범위를 벗어난다.
  • 코드를 작성하기 전에 테스트 케이스를 작성하는 것과 코드를 작성한 후에 테스트 케이스를 작성하는 것은 시간과 노력 면에서 같지만, 전자가 결함-발견-디버그-수정 주기를 줄여준다.
  • 아무리 여러 가지 테스트를 고려해 봐도 테스트는 좋은 소프트웨어-품질 프로그램의 한 부분일 뿐이다.
  • 결정적으로 기초 테스트와 데이터 흐름 분석, 경계 분석, 나쁜 데이터, 좋은 데이터를 사용하여 많은 테스트 케이스를 생성할 수 있다. 오류 추측으로 테스트 케이스를 추가로 생성할 수 있다.
  • 오류는 오류를 유발할 가능성이 있는 몇 개의 클래스와 루틴에 쏠려있는 경향이 있다. 오류를 유발할 가능성이 있는 코드를 찾아서 재설계한 다음 다시 작성한다.
  • 테스트 데이터가 테스트하는 코드보다 오류를 더 자주 발생시키는 경향이 있다. 코드를 개발하는 것 못지않게 테스트 개발에 주의를 기울여 그런 문제를 피하도록 한다.
  • 자동 테스트는 일반적으로 유용하며 회귀 테스트에서는 필수적이다.
  • 규칙적으로 진행하고 측정하고 테스트를 향상시키기 위해서 배운 것을 사용하는 것이 테스트 프로세스를 향상시키는 최고의 방법이다.

results matching ""

    No results matching ""