Systematic Testing
Systematic testing은 테스트를 시스템처럼 조직적으로 수행하기 위한 절차를 의미한다. 주요 구성 요소는 다음과 같다.
- 테스트 케이스 선택 및 생성
- 테스트 실행과 결과 문서화
- 결과 평가 (가능하다면 자동화)
- 테스트 종료 시점 결정 (언제 "충분히" 테스트했다고 볼 것인가?)
완전한 테스트는 현실적으로 불가능하기 때문에, 특정 criterion을 정의하고 그에 따라 테스트를 수행한다. (예를 들어, 모든 if 문이 true/false 양쪽으로 한 번씩 실행되도록 테스트하기)
Kinds of Tests: Black Box vs White Box Testing
Testing은 black box testing과 white box testing으로 나눌 수 있다.
Black box testing은 코드를 보지 않고 요구사항과 명세서를 기반으로 테스트를 작성하는 기법이다. 이는 개발 과정과 병렬로 진행할 수 있기 때문에 대형 프로젝트에서 유리하다.
White box testing은 코드를 보고 실제 로직과 구조를 기반으로 테스트를 작성하는 기법이다.
Black Box Testing
Black box testing은 functional specification 정보를 사용한다.
Functional Specification
소프트웨어 요구사항은 일반적으로 다음 세 가지를 포함한다.
- 입력: 어떤 입력을 받을 수 있는가.
- 행동: 입력을 처리할 때 어떤 동작을 해야 하는가.
- 출력: 어떤 출력을 생성해야 하는가.
이 세가지 요소를 기반으로 세 가지 black box test가 생긴다.
Three Kinds of Black Box Methods
입력 공간을 분석해서 입력에 대한 테스트를 체계적으로 커버하는 기법
테스트 기법은 다음과 같다.
- Exhaustive Testing: 입력 가능한 모든 조합을 테스트하는 방법
- 예를 들어 32-bit 정수 두 개를 입력으로 받는 경우 약 1.6×1019개의 조합이 존재한다. 이처럼 현실적으로 불가능한 경우가 대부분이다.
- 그러나 입력 도메인이 0과 99 사이의 값으로 제한된 경우 자동화가 가능하다. 완전 검사가 가능한 경우는 매우 드물다.
- Input Partitioning: 입력 공간을 의미 있는 그룹으로 나누고, 각 그룹에서 대표 입력값을 선택하는 방법
- 전형적이며 가장 많이 쓰이는 black box 기법이다.
- 두 정수 x,y를 입력받아, x 이하의 수 중 y로 나누어 떨어지는 모든 수를 출력하라. 단, x 또는 y가 0이면 0만 출력해야 하는 경우 입력을 x=0,y=0,x=0∩y=0 같은 특수 케이스로 ㄴ누어 양수와 음수까지 확장 가능한 partition으로 나눈다.
- Partitioning은 입력 종류를 모두 테스트 했는지 명확하게 알 수 있다. 또한 여러 관점에서 partition할 수 있어 설계 실수도 발견할 수 있다. Partitioning을 통해 단순 대표 입력만 테스트해도 충분한 신뢰를 확보할 수 있다.
- Shotgun Testing: 랜덤 입력을 다량 생성하는 방식의 난수 기반 테스트
- 예기치 못한 입력 조합을 탐색할 수 있고, 특정 버그를 운 좋게 발견할 수 있다. 하지만 체계성이 없고 언제 테스트가 끝났는지 판단할 수 없다.
- 이러한 이유로 shotgun은 단독으로 쓰기 보다는 input partitioning을 강화하는 데 사용한다. 즉, 각 partition 안에서 다양한 random 입력을 추가로 테스트한다.
- Input Robustness Testing: 입력이 잘못되어도 프로그램이 죽지 않는지 확인하는 테스트로, random garbage input과 boundary value testing 두 가지가 있다.
- Input Boundary Testing: 실제 프로그램 오류가 주로 입력 범위 경계에서 발생한다. Boundary testing은 체계적이며, 경계값을 모두 테스트하면 종료한다는 명확한 기준이 있다.
Output Coverage Testing
출력 가능한 값들을 분석해서 각각을 유도하는 입력을 설계하는 기법으로, 입력 중심이 아닌 출력 도메인을 기반으로 테스트한다. 이때 요구사항을 깊이 있게 분석해야 하기 때문에 상당히 복잡하지만 문제 발견 능력은 매우 뛰어나다. 또한 출력의 종류가 적을 때에는 exhaustive output testing이 가능하다.
- Input coverage와 비교해보면, 예를 들어 "두 입력이 같으면 1, 다르면 0을 출력"해야 하는 요구사항이 있을 때 input partitioning으느 수많은 입력 경우의 수를 따져보아야 하지만, output coverage는 0 또는 1 두 개의 테스트로 완전 커버가 가능하다.
Output partition만 설정하면 끝이 아니다. 각 partition을 발생시키는 입력을 역으로 찾아야 한다. 이 과정이 매우 어렵고 시간도 오래 걸린다. 하지만 이 과정에서 다음과 같은 문제를 발견할 수 있다.
- 특정 출력 class가 실제로는 발생할 수 없음
- 요구사항이 애초에 잘못 정의됨
- partition이 잘못 설계됨
입력 파일이 여러 개거나 출력이 여러 개라면, input stream, output stream을 독립적인 partition 그룹으로 보고 각각에 대해 별도의 input/output partitioning을 수행해야 한다.
Functionality Coverage Testing
요구사항을 작은 단위로 나누어 각 기능 단위를 독립적으로 테스트하는 기법
요구사항은 보통 여러 기능이 묶여 있지만 testability는 기능 단위로 분할할 때 높아진다. 이를 requirement partitioning이라고 한다.
예를 들어, 요구사항이 "두 정수 x,y를 입력받아, x 이하의 수 중 y로 나누어 떨어지는 모든 수를 출력하라. 단, x 또는 y가 0이면 0만 출력한다."라고 한다면 요구사항은 다음과 같이 분해된다.
- 두 정수를 입력받아야 한다.
- 출력은 0개 이상의 정수여야 한다.
- 출력값은 모두 x 이하여야 한다.
- 출력값은 모두 y로 나누어 떨어져야 한다.
- 조건을 만족하는 모든 값을 출력해야 한다.
- x=0 또는 y=0이면 출력은 무조건 0 하나여야 한다.
이 여섯가지 요구사항에 대해 테스트 케이스가 각각 수 개씩 만들어진다. 그 중 조건을 만족하는 가장 단순한 케이스로 테스트를 진행한다.
Functionality Coverage는 요구사항 기반 테스트이므로 acceptance test와는 다르다. 요구사항을 독립 요소로 취급하지만 실제로는 기능 간 상호작용이 존재할 수 있으므로 완전 대체할 수 없다. 그래도 테스트 완료 기준이 명확하다. (각 요구사항에 대해 최소 하나의 테스트가 존재할 것)
Test Case Design의 원칙
Black box testing은 실험(experiment)이다. 따라서 실험 설계 원리를 따른다.
- 변수(원인)를 분리하여 관찰해야 한다.
- 테스트 입력을 단순하게 유지하고, 하나의 입력만 변화시키며 나머지는 동일하게 유지해야 한다.
- 불필요한 변동을 제거해야 한다.
- 임의의 복잡한 입력을 넣는 것은 오류 원인을 찾기 어렵게 만든다.
(가장 간단한 테스트를 사용하지 않으면 검증이 어려워진다.)