안녕하세요. Glenn입니다.
지난번에 썼던 Testing 관련 독후감 1편에 이은 2편입니다.
오늘은 white box testing 과 black box testing에 대한 개념 그리고 테스트 케이스 생성 방법에 대한 부분을 요약정리 하였습니다.
테스팅에 관심을 두신 개발자 분들에게 도움이 되었으면 합니다.
------
화이트 박스 테스팅 (White-box Testing)
화이트 박스 테스팅은 테스트 케이스가 프로그램의 논리를 실행하고 포함하는 정도에 초점을 맞춘 테스팅으로 프로그램 코드의 모든 경로를 실행하는 것을 목표로 한다. 다만 작은 프로그램이야 모든 경로가 실행 가능하겠지만 로직의 덩치가 커지거나 루프가 있는 프로그램의 경우 현실적으로 불가능하다. 그래서 어느 정도 절충하여 위와 같은 방법론들이 나오게 되었는데 가장 약한 방법이 문장 커버리지이다.
문장 커버리지(Statement coverage)는 프로그램 내 모든 문장을 적어도 한번 실행하는 것으로 내부 조건문을 검사하는 것이 아니므로 의미가 없는 테스트라고 뵬 수 있다. 이것보다 한 단계 높은 커버리지 기준이 결정 커버리지 (Decision coverage) 또는 분기 커버리지(Branch coverage)이다. 이 기준은 모든 결정이 적어도 한번은 참 또는 거짓 값을 갖도록 테스트 케이스를 작성하는 방법이다. 다시 말해 모든 분기 방향을 적어도 한번은 실행하는 것으로 문장 커버리지 조건도 함께 만족시킬 수 있다.
다만 이를 위해서 결정 커버리지 테스트 케이스는 if 혹은 while 문과 같은 곳에서 참과 거짓을 결과를 가질 수 있어야 한다. 다만 이 역시 조건 문 내의 상세 조건들을 개별적으로 판단하지 못하므로 이 결정 커버리지가 약하다고 볼 수 있다. 이를 보완한 것이 조건 커버리지로 if 및 while문 등과 같은 조건 내의 상세 조건들의 결과가 참 또는 거짓을 최소 한번은 발생하게 테스트 케이스를 작성해야 한다. 물론 이 조건 커버리지가 결정 커버리지를 어느 정도 포함은 하고 있지만 모든 경우는 포함하지 못한다.
가령 예를 들어 IF (A && B) THEN X의 문장이 있다면 A=1, B=0 과 A=0, B=1을 하면 조건 커버리지는 만족하지만 문장 X는 실행하지 못하므로 결정 및 문장 커버리지를 만족시키지 못하는 문제점을 지닌다. 이런 문제점을 보완하기 위해서 결정/조건 커버리지가 존재한다. 결정/조건 커버리지 (CDC coverage)는 결정 내의 모든 조건을 최소한 한 번씩 만족하고, 모든 결정의 결과를 최소한 한 번씩 만족하며 시작점을 최소한 한 번씩 실행하는 것이다. 다만 이 방법론의 유일한 구멍은 다중 조건의 경우 앞 조건이 충족되었을 때 뒤 조건은 무시되는 경우가 있어 빠지는 경우가 존재한다. IF (A or B) THEN X의 경우 A가 1 이면 B가 1이든 0이든 관계없이 문장 X가 실행되는 것이다. 그래서 조건 커버리지 및 결정/조건 커버리지 기준으로 논리 표현의 에러를 발견하지 못할 수 있다.
이 문제를 해결하는 것이 다중조건 커버리지이다. 이 기준은 모든 결정에서 가능한 모든 조건 결과의 조합과 모든 시작점을 최소한 한번은 실행하는 충분한 테스트 케이스 작성을 요구한다. 다만 너무 많은 테스트 조건이 나올 수 있으므로 적절히 사용해야 할 필요가 있다.
블랙 박스 테스팅 (Black-box Testing)
프로그램을 테스트할 때는 가능한 입력의 부분 집합을 제한적으로 사용한다. 물론 에러를 찾을 가능성이 가장 높은 부분 집합을 선택한다. 이런 선택된 부분 집합은 아래의 두 가지 특성을 지녀야 한다.
1. 미리 정의된 적절한 테스팅 목표를 만족하며 다른 테스트 케이스를 하나 이상 줄인다
필요한 테스트 케이스 수를 가능한 최소화 하면서도 서로 다른 입력 고려 사항을 다뤄야 한다.
2. 가능한 다른 테스트 케이스의 큰 집합과 관계한다.
프로그램의 입력 도메인을 한정된 수의 동등 클래스로 나눠서, 각 클래스를 대표하는 값이 해당 클래스 내의 다른 테스트 값과 동등하다는 가정을 할 수 있어야 한다.
위 두 가지 특성을 고려한 블랙 박스 방법론이 동등 분할법이다. 첫번째는 조건을 포함하는 테스트 케이스의 집합을 최소화할 때 사용되며, 두번째는 테스트할 관심 있는 조건의 집합을 개발할 대 사용된다. 즉 이것을 동등 분할법에 대입시키면 첫 번째는 테스트 케이스 정의, 두번째는 동등 클래스 식별과 연결된다.
동등 분할 클래스 (Equivalent Partitioning)는 명세의 문장이나 단락 같은 입력 조건을 식별해 2개 또는 그 이상의 그룹으로 나눈 것이. 동등 클래스는 유효 동등 클래스와 무효 동등 클래스로 나누어 지는데 유효 동등 클래스는 프로그램의 유효한 입력 값 (Positive Test)를 보여주며 무효 동등 클래스는 유효하지 않고 예측할 수 없는 조건에 초점을 맞춘 무효한 입력 값 (Negative Test)를 보여준다.
이제 동등 클래스를 사용해 테스트 케이스를 식별하는 방법은 아래와 같다.
- 각 동등 클래스에 고유의 수를 할당한다.
- 테스트 케이스가 모든 유효한 동등 클래스를 포함할 때까지, 포함하지 않는 모든 유효한 동등 클래스 수만큼 새로운 테스트 케이스를 작성한다.
- 테스트 케이스가 모든 무효한 동등 클래스를 포함할 때까지, 포함되지 않는 각각의 무효한 동등 클래스를 위한 새로운 테스트 케이스를 작성한다.
이렇게 각각의 테스트 케이스가 무효한 케이스를 포함해야 하는 이유는 특정 입력은 결정/조건 커버리지의 예처럼 다른 잘못된 입력 확인을 감추거나 대치하기 때문이다. 위와 같이 동등 분할을 사용하여 테스트 케이스를 만들 때 여러 값을 선정할 수 있다. 다만 동등 분할에 필요한 값을 선정함과 동시에 몇 가지 조건을 추가하면 경계 값 분석(Boundary Value Analysis)이라는 경계 조건을 탐색한 성공률 높은 테스트 케이스를 생성할 수 있다. 그 조건이란 다름과 같다.
- 대표하는 값으로 동등 클래스의 임의 요소를 선택하기보다, 경계 값 분석에서는 동등 클래스의 경계에 있는 값을 테스트 대상이 되는 요소로 선택한다.
- 입력 조건에만 초점을 맞추기보다 결과 영역 (출력 동등 클래스)도 고려해 테스트 케이스를 도출한다.
다만 경계 값 분석을 위한 명확한 가이드라인 제시는 불가능하다. 다만 몇 가지 일반적인 지침은 아래와 같이 존재한다.
- 입력 조건이 값의 범위를 명시하고 있다면, 범위 끝의 테스트 케이스와 범위 끝을 벗어나는 무효한 입력 테스트 케이스를 작성
- 입력 조건이 값의 수를 명시하고 있다면 최소 값, 최대 값, 최소 값보다 작은 값, 최대 값보다 큰 값의 테스트 케이스를 작성
- 1번 지침을 출력 조건에 적용
- 2번 지침을 출력 조건에 적용
- 프로그램의 입력과 출력이 순차 파일, 선형 리스트나 테이블과 같이 순서화된 집합이라면, 집합의 첫 번째와 마지막 항목에 주목
위 지침을 기반으로 경계 값 분석과 동등 분할의 중요한 차이점을 보면, 경계 값 분석이 동등 분할의 경계와 주변 상황을 탐색한다는 것이다. 경계 값 분석과 동등 분할은 입력 환경의 조합을 탐색하지 못하므로 조합의 수가 일정 수 이상을 넘으면 멈출 것이다. (Memory overflow, file size limitation, etc.) 또한 입력 조건을 동등 분할해도 조합의 수는 천문학적이기 때문에 입력 조합의 테스팅은 단순한 작업이 아니다. 원인-결과 그래핑은 테스트 케이스의 집합을 체계적으로 선택하는데 도움이 되며 부수적으로 명세의 불완전성과 모호성을 찾아낼 수 있다. 이 방법은 자연어 명세를 정규형 언어로 변환한다. 실제 그래프는 디지털 논리 회로이지만 표준 전자 표기법 대신 단순한 표기법을 사용한다. 또한 논리 연산자 같은 Boolean 논리의 이해가 요구된다. 원인 결과 그래핑은 명세에서부터 시작되므로 테스트 케이스를 유추하기 위해서는 다음 절차가 필요하다.
- 명세를 한 줄, 한 줄 분석하여 작업할 수 있는 크기로 나눈다.
- 명세에서 원인과 결과를 식별한다. 원인은 명확한 입력 조건이거나 입력 조건의 동등 클래스이며, 결과는 출력 조건이거나 시스템 변형이다. (원인과 결과를 설명하는 명세를 읽으면서 단어, 숨겨진 단어, 구문에서 원인과 결과를 식별해야 함)
- 명세의 의미론적 내용을 분석하고, 원인과 결과를 연결하는 불린 그래프로 변환한다.
- 구문적이나 환경적 제한 조건 때문에 원인-결과 조합이 불가능한 경우에 대한 설명이 그래프에 주석으로 달린다.
- 체계적으로 그래프의 상태 조건을 추적함으로, 그래프를 제한된 입력 결정 테이블로 변환할 수 있다.
- 결정 테이블의 열을 테스트 케이스로 변환한다.
즉 명세서를 분석하여 원인에 해당하는 입력 조건과 그것의 출력 결과를 논리적으로 연결한 그래프를 작성하고 의사 결정 테이블로 바꾼 후 테스트 케이스를 개발하는 것이다. 위와 같은 여러 가지 툴을 사용하여 테스트를 진행하는 것도 좋지만 어떤 사람은 본능적으로 프로그램 테스팅에 익숙하게 보이기도 한다.
이런 사람들은 위에서 설명한 특별한 방법론을 사용하지 않아도 에러를 찾아내는 능력을 가지고 있다. 특정 프로그램이 주어지면 직감과 경험으로 가능한 형태의 에러를 추측하고 에러를 밝혀 내기 위해 테스트 케이스를 작성한다. 이를 에러 추측 기법이라고 불리며 사람에 따라 테스트 케이스의 품질이 현저히 차이가 난다.
아무래도 직감으로 테스트 케이스를 작성하는 것이기 때문에 절차와 같은 것들이 딱히 정해져 있지 않다. 다만 테스트 케이스 작성자가 에러가 발생하기 쉬운 상황을 리스트하고, 거기에서 테스트 케이스를 만들어 내는 것이 기본이다. 지금까지 여러 테스트 케이스 설계 방법론을 정리했다. 이 방법론들은 앞서 설명했듯이 서로 상호 보완적인 관계로 사용 되어야 최적의 테스트 케이스를 만들 수 있다. 그렇다면 최적의 테스트 케이스를 만들어 내기 위해서는 어떤 전략을 사용해야 할까?
- 명세가 입력 조건의 조합을 기술하고 있다면, 원인-결과 그래핑부터 시작하라.
- 중요한 부분에서는 입력과 출력 경계 값을 분석하라.
- 입력과 출력의 유효한 동등 클래스와 무효한 동등 클래스를 식별하고, 필요하다면 추가 테스트 케이스를 식별하라.
- 부가적인 테스트 케이스를 추가하기 위해 에러 추측 기법을 사용하라.
- 테스트 케이스의 집합과 관련된 프로그램 논리를 조사하라.
이런 전략을 사용한다고 모든 에러를 발견할 수 있는 것은 아니지만 논리적으로 타협할 수 있을 만큼 발견하는 것이 가능하다.
테스트 매니징을 할 예정이며 현재 프로그래머로써 제품을 상용화 시키는데 테스트 케이스를 논리적으로 타협해야 한다는 점이 마음에 들지는 않지만 테스트에 들어가는 비용과 시간을 만족시키기 위해서는 어쩔 수 없는 선택인 것 같다.
본인도 테스트 케이스를 생성하기 위해서 여러 방안을 검색하여 도입 해보려고 노력 했으나 비용 및 시간의 틀에 갇혀 만족 할 만한 테스트 케이스를 만들어 내지를 못했다. 지금과 같은 테스트 케이스 작성 전략에 대해 미리 알았더라면 충분히 만족할 만한 테스트 케이스를 작성할 수 있었을 것이라 생각한다.
지금까지 현 상황에 적절한 테스트 케이스를 생성하는 방법에 대해 알아봤다. 그렇다면 이 테스트 케이스를 어떻게 적용하여 테스트를 수행 할 것인지 알아보겠다.
-----------------------------------
다음편에 계속~