Structural testing에 대한 이해
A사 techbard
서론
현대의 소프트웨어 테스팅에 있어 두 가지 테스트 디자인 접근법이 존재한다. 그 하나는 요구 사항의 내용만을 검증하려는 의도를 가지고 요구 사항을 분석해서 테스트 케이스 디자인에 이용하는 방식이고, 또 다른 하나는 요구 사항의 내용뿐만 아니라 소프트웨어 내부의 구조가 올바른지를 검증하려는 의도를 가지고 소프트웨어의 내부 구조를 분석해서 테스트 케이스 디자인에 이용하는 방식이다. 일반적으로 요구 사항만을 검증하기 위한 테스트 접근 방법은 블랙 박스 테스팅이나 requirement-based testing 그리고 functional testing, 또는 요구 사항에 대한 분석을 의미하는 requirement coverage analysis 등으로 불리기도 한다. 반면에, 이른바 화이트 박스 테스팅으로 알려져 있는 방식은 structural testing, 또는 소프트웨어 내부 코드에 대한 분석을 의미하는 code coverage analysis 등으로 알려져 있다.
이번 기고에서는 이러한 두 가지 접근법 중 구조에 기반한 테스팅에 대해서 알아보도록 하자. 이후로 이러한 구조에 기반한 테스팅을 왜 수행해야 하는지, 요구사항과 구조 기반 접근법의 강점과 약점, 코드 (테스트) 커버리지의 정의와 control flow graph와 논리식을 이용한 커버리지의 소개, MC/DC 커버리지의 소개, 이에 대한 산출 방법 등을 소개하도록 하겠다.
본론
용어 사용에 대한 고려
먼저 용어 사용에 대한 문제부터 생각해 보자. 일반적으로 소프트웨어 테스팅 업계에서 사용되는 블랙 박스 테스팅과 화이트 박스 테스팅은 그 용어의 강렬하고 극명한 느낌 때문에, 널리 사용되고 있으나 그 용어로부터 테스팅에 적용되는 사고의 폭이 좁아진다고 생각된다. 검은색 아니면 흰색의 이분법으로 테스팅의 사고를 제한하면, 테스터들이 자신들의 테스팅 사고의 스펙트럼에 화이트 박스 접근 방법의 여러 가지 개념을 도입하려고 하지 않기 때문이다. 예를 들면, 이후로도 소개를 하겠지만 cause-effect graphing 또는 decision table로 알려져 있는 이른바 블랙 박스 테스팅 디자인 기법은 최적의 테스트 개수와 빠짐 없는 테스트 케이스를 추출해 내는 기법으로 사실 화이트 박스 테스팅 기법에서 사용되는 TRUE와 FALSE의 논리를 이용해 논리적으로 부정되는 테스트 케이스를 제거하는 기법이다. 즉 decision table 기법은 화이트 박스 테크닉을 소프트웨어 내부 구조 (코드)에 적용하는 것이 아니라 블랙 박스 테크닉의 주 고려 대상인 논리적인 요구 사항에 적용해 테스트 케이스를 디자인하는 것이다. 결국 이것은 화이트 박스 테크닉을 블랙 박스 테스팅에 사용한 것으로 볼 수 있다. 따라서, 현대의 테스터는 좁은 틀에 붙잡혀 테스팅 사고의 폭을 제한할 것이 아니라 여러 가지 개념을 차용해서 블랙 박스와 화이트 박스에 구애 받지 않고 가장 효율적인 방식의 테스팅을 수행할 필요가 있다. 따라서, 블랙 박스, 화이트 박스의 이분법이 적당하지 않다고 보기 때문에 이 기고에서는 블랙 박스와 화이트 박스를 요구 사항 기반의 테스팅, 소프트웨어의 구조에 기반한 테스팅으로 구분해서 언급하도록 하겠다.
구조에 기반한 테스팅을 수행해야 하는 이유와 상황
일단 요구 사항에 기반하여 테스팅을 디자인 했다고 가정하면, 요구 사항과 테스트 케이스와의 추적 매트릭스 (traceability matrix)를 작성하는 경우 요구 사항의 내용에 대해서는 빠짐없이 테스트 케이스를 작성했다고 볼 수 있으므로 디자인된 테스트가 완전하다고 가정할 수 있다. 하지만, 요구 사항 기반의 테스트는 태생적으로 요구 사항을 기반으로 해서 소프트웨어의 입력 값과 출력 값을 정의해, 소프트웨어의 동작을 시험하는 일련의 테스트 케이스를 만들어 내고 수행하는 테스트 방법이므로, 요구 사항 자체의 불완전성에 기인해서 완전하게 되지 못할 가능성이 높다. 대개 요구 사항 자체는 불완전하거나 모호한 것이 현실이므로 이러한 상황은 요구 사항 기반의 테스팅으로는 소프트웨어 내부에 잠재하고 있는 모든 결함을 노출시키기 어렵게 된다.
예를 들어, Glenford Myers는 소프트웨어 테스팅의 고전인 그의 저서에서(1) 프로그래머들을 대상으로 일명 삼각형 문제 (triangle problem)라고 불리는 “프로그램이 3개의 값을 읽어 삼각형 세변의 길이로 인식해서 이 삼각형이 부등변 또는 이등변 또는 정삼각형인지 또는 삼각형으로 부적합한 값인지 출력하는 프로그램을 시험하는 테스트 케이스를 작성하라.” 라는 명제를 제시했으나 대부분의 프로그래머 들이 완전한 테스트 케이스 들을 작성하지는 못했다고 말하면서 요구 사항에 기반을 둔 소프트웨어 테스팅 접근 방법의 문제 검출 능력을 간접적으로 보여주고 있다.
구조에 기반한 테스팅을 수행해야 하는 또 다른 이유는 요구 사항 기반의 테스팅 접근 방법이 간과하는 문제들이 있다는 점이다. 예를 들면 모든 결함들 중에서 로직 (logic) 에러의 비율이 32% 정도로 조사되었는데(2), 요구 사항 기반의 접근 방법은 이러한 유형의 결함에 대해서 검출 능력이 뛰어나지 않다. 이는 요구 사항 기반 접근 방법의 근원적인 속성에 기인하는데, 이 방식은 요구 사항을 분석해서 입력 값들을 선별하고 입력한 결과를 관찰해 요구 사항에 일치하는지 확인하는 방법이기 때문이다. 동일한 요구 사항이더라도 구현하는 방법에 따라 여러 가지 논리가 사용될 수 있기 때문에 당연히 요구 사항만의 정보로는 구현된 소프트웨어 내부 구조의 논리를 시험하기에 적합하지 않거나 일부의 논리만 시험할 가능성이 크다.
또한, 앞에서도 계속 언급하지만 요구 사항 자체의 불완전성 때문에 요구 사항에 기반한 테스트 디자인은 테스트 케이스가 완전성을 가지는지 확인할 수 있는 지표를 제공하지 않는다. 앞에서도 언급했던 요구 사항 리스트와의 추적 매트릭스 (traceability matrix)로 이를 보완할 수 있겠지만, 요구 사항과 개개의 항목들의 연결만 나타낼 수 있기 때문에, 소프트웨어 내부의 논리 구조에 따른 요구 사항의 불일치 가능성에 대해서는 생략될 수 밖에 없다. 이러한 상황이기 때문에 구조적인 테스트 디자인에서는 코드 커버리지 (code coverage)라는 개념을 사용해 테스트 케이스의 완전성을 나타내고 있다.
또 다른 고려 사항도 존재하는데 소프트웨어 테스팅에 있어서 요구 사항의 존재 및 정확성은 매우 중요하다. 대부분의 경우 소프트웨어의 수정 사항은 요구 사항에 대해서 일치하도록 만드는 과정이기 때문에, 요구 사항 자체가 존재하지 않는 소프트웨어의 동작은 항상 옳을 수 밖에 없다. 결론적으로, 요구 사항에 대해서 일치하는지 불 일치하는지 테스트 해야 하는 요구 사항 자체가 존재하지 않는 소프트웨어는 어떤 동작을 수행해야 하는지, 어떤 동작을 수행해야 하지 않아야 하는지에 대해서 알 수 없으므로 완성된 소프트웨어의 동작이 그대로 옳은 것으로 받아들여 지기 쉽다. 따라서, 요구 사항이 불완전한 소프트웨어 개발 프로세스의 경우 요구 사항에 기반한 테스트 디자인 자체를 수행할 수 없다. 이런 경우에는 디자인된 소프트웨어의 구조에 기반해 테스트를 디자인 할 수 밖에 없기도 하다.
요구 사항 기반과 구조 기반 테스팅의 장점과 단점
이 두 가지 접근 방법의 장점과 단점을 알아보기 전에 일반적인 소프트웨어 개발 프로세스의 단면을 살펴보자.
그림 1. 소프트웨어 개발 라이프 사이클의 일부분
당연한 얘기지만, 요구 사항 기반의 테스트 디자인이 구조 기반의 테스팅 디자인 보다 먼저 시작될 수 있다. 그러므로, 요구 사항 기반의 테스트 디자인은 요구 사항만 존재해도 시작할 수 있다. 반면에 구조 기반의 테스트 디자인은 디자인 및 코딩이 완료된 이후에나 시작할 수 있기 때문에 상대적으로 늦게 시작될 수밖에 없다. 하지만, 개발 프로세스 상의 마지막으로 진행할수록 프로젝트 데드라인의 압박과 테스트 해야 할 분량이 과도하게 되면서, 구조에 기반한 테스트 디자인을 할 수 있는 기회가 줄어드는 것이 사실이다. 하지만, 이것이 요구 사항 기반의 테스트 디자인만을 해야 하는 이유가 되는 것은 아니다.
그렇다고 구조 기반의 테스트 디자인만이 전부일까? 그렇지 않다. 만일 요구 사항 기반의 테스팅을 수행하지 않고 코드 구조를 완전하게 동작시켜 보는 구조 기반의 테스트만 수행했다 하더라도 아예 구현조차 되어 있지 않은 코드에 대해서는 구조 기반의 테스트만으로는 완벽한 테스트를 했다는 결론을 내릴 수 없다는 점이 당연하다. 이것이 요구 사항 기반의 테스트와 구조 기반 테스트를 상호 보완적으로 사용해야 하는 이유이다. 반드시 요구 사항 기반의 테스트 디자인을 수행한 후 다시 구조 기반으로 보강하는 것이 가장 완전한 (결함 검출이 뛰어난) 테스트로 알려져 있다. (1) (3)
이미 요구 사항 기반의 디자인이 가질 수 있는 단점이 구조 기반의 디자인으로 보완되고, 다시 구조 기반의 디자인의 단점이 요구 사항 기반의 디자인으로 보완된다는 점은 전술한 바와 같으며 요약하면 다음과 같다.
요구 사항 기반의 테스트 디자인은 필수적이기는 하지만 테스트의 완전성이 상대적으로 떨어지며, 구조 기반의 테스트 디자인은 요구 사항을 놓칠 가능성이 있다. |
이제 요구 사항 기반의 디자인 기법의 단점과 구조 기반의 테스팅 디자인 기법이 이러한 단점을 보완할 수 있다는 점을 살펴보자. (3)
요구 사항 기반의 테스트 디자인은 다음과 같은 단점이 존재한다.
소프트웨어 요구 사항은 완전하지 않을 뿐더러 실행 코드가 보여줄 수 있는 모든 동작에 대해서 자세히 언급되어 있지 않다.
소프트웨어 요구 사항은 테스트해야 하는 소스 코드가 보여주는 다양한 기능적인 동작에 대해서 올바른지 또는 올바르지 않은 지를 확인할 수 있는 정보를 담고 있지 못하다.
요구 사항 기반의 테스트 디자인은 코드가 요구 사항을 담고 있다는 점을 보여줄 수는 있어도, 요구 사항에서 의도하지 않은 기능을 구현하고 있다는 점을 증명하기는 어렵다.
구조 기반의 테스트 디자인은 다음과 같이 요구 사항 기반의 테스트 디자인을 보완한다.
적절한 커버리지 수준으로 테스트가 수행되었음을 보증한다.
의도하지 않은 기능이 포함되어 있지 않음을 간접적으로 증명하는 방법을 제공한다.
요구 사항 기반의 테스트 디자인 자체의 커버리지를 측정할 수 있게 한다. (하지만 테스트 케이스의 커버리지를 측정해서 특정 수준을 만족하는 것이 곧 소프트웨어 코드 자체의 품질을 의미하지는 않는다.)
실제적인 pseudo code의 예제를 가지고 요구 사항 기반과 구조 기반의 개념에 대해서 좀 더 알아보도록 하자.
Pseudo code를 이용한 요구 사항 기반 테스팅의 약점 소개
다음과 같은 요구 사항에 맞게 각각의 두 가지 디자인 관점으로 테스트를 디자인하는 사례를 살펴보자.
온라인 구매 사이트에서 구매자가 현금 결제를 하며 회원으로 가입된 경우 5%의 할인을 받으며 이에 해당되고 않고 학생인 경우 1%의 할인을 받는다. |
위의 요구 사항을 pseudo code로 나타내면 다음과 같을 것이다.
IF cash = “yes” and membership = “yes”
THEN 5% discount
ELSE IF student = “yes”
THEN 1% discount
ELSE no discount
논리적으로 이 요구 사항을 분석하기 용이하게 하기 위해서 다음과 같은 control flow graph로 다시 나타내 보자. Control flow graph는 (이하 CFG로 표기) 프로그램의 제어 구조의 흐름을 표현하는데 사용하는 방법으로 대개 다음과 같은 표기법이 사용된다.
그림 2. Control flow graph 표기법
위의 요구 사항은 다음과 같은 CFG로 나타낼 수 있다.
그림 3. 예제 pseudo code의 control flow graph
이제 위의 CFG와 pseudo code를 참고해 요구 사항 기반의 관점으로 테스트 케이스를 작성해 보자. 요구 사항의 문구만을 염두에 두고 테스트 케이스를 작성한다면 다음과 같은 경로 (path)의 테스트 케이스가 산출될 것이다.
A -> B
C -> D -> E
C -> F
단 세 개의 테스트 케이스 만으로 완전하게 테스트를 할 수 있을 것으로 보인다. 하지만, 코드에서 (A AND B)가 아닌 (A OR B)로 구현되었을 가정을 확인해 보자.
위의 세 개의 테스트 케이스로 이 결함을 발견해 낼 수 있을까? 위의 path를 만족하는 원인 결과의 조건 표로 기술하면 다음과 같다. (T는 true, F는 false를 나타낸다.)
표 1. 단순히 요구 사항 문구에 의존해 작성된 테스트 케이스
|
1 |
2 |
3 |
Cash |
T |
F |
F |
Membership |
T |
F |
F |
Student |
- |
T |
F |
5% discount |
× |
- |
- |
1% discount |
- |
× |
- |
위의 테스트 케이스 대로 입력하면, 모두 문제가 없어 보인다. (이것이 문제다!) (A AND B)와 (A OR B)의 진리표(true table)는 TT / FF에 대해서는 동일한 결과값을 가지기 때문이다. 하지만 (A OR B)로 구현되어 있다면 결함을 검출할 수 없다. 따라서, 이것이 완전한 테스트 케이스라고 할 수 없다. 우리가 CFG에서도 놓치고 있는 부분이 있는데 그것은 (A AND B)의 논리 조건이라는 점이다. 이것이 AND 조건 관계이기 때문에, TT / FF 이외에 TF / FT의 조합을 더 가질 수 있다. 이러한 사실을 염두에 두고 다시 한 번 모든 경로 (path)를 만족하는 경우를 원인 결과의 조건 표로 기술하면 다음과 같다.
표 2. 요구 사항의 논리를 따져서 작성된 테스트 케이스
|
1 |
2 |
3 |
4 |
5 |
6 |
7 |
Cash |
T |
T |
T |
F |
F |
F |
F |
Membership |
T |
F |
F |
T |
T |
F |
F |
Student |
- |
T |
F |
T |
F |
T |
F |
5% discount |
× |
- |
- |
- |
- |
- |
- |
1% discount |
- |
× |
- |
× |
- |
× |
- |
이제 (A or B)로 잘못 구현된 코드를 가지고 이 일곱 가지 테스트 케이스를 입력한 경우 어떻게 되는지 살펴보자. (숫자는 테스트 케이스 번호를 의미한다.)
결함을 검출 할 수 없는 정상적인 값을 출력한다. (이미 앞에서 TT / FF의 조합만으로는 AND/OR 조건을 구별해 낼 수 없다는 점을 배웠다.)
예상 결과 값은 1% discount인데, (A or B)로 잘못 구현되었다면, 5% discount 결과값을 보이므로 결함을 검출해 내었다!
예상 결과 값은 no discount인데, (A or B)로 잘못 구현되었다면, 5% discount의 결과 값을 보이므로 결함을 검출해 내었다!
4), 5)번 케이스에 대해서도 모두 결함 검출이 가능하다.
6), 7번 케이스에서는 결함을 검출할 수 없는 정상적인 값을 출력한다. (TT / FF의 조합만으로는 AND/OR 조건을 구별해 낼 수 없다.)
즉, 요구 사항에 기반한 테스트 케이스 디자인은 요구 사항의 불완전성에 더해져서 소프트웨어 내부의 구조를 완전히 수행하지 못하므로 불완전한 테스트 케이스를 생성해 낼 가능성이 있으며 오히려 오류가 존재하는 테스트 결과를 만들어 낼 수 있다! 특히, 위의 CFG에서는 AND 조건이 흐름 (flow)상 맨 위에서 출발하였기 때문에 조합의 경우의 수가 상대적으로 적지만, 이러한 논리식이 구조 흐름의 중간에 여러 개 존재한다면 각각이 가질 수 있는 독립적인 경로 (independent path)의 수는 기하급수적으로 늘어나게 된다. 따라서, 테스터는 구조 기반의 테스트 케이스 디자인 기법을 익혀야만 완전한 테스트 케이스를 작성할 수 있다.
논리식을 통한 커버리지 개념의 소개
이제 코드 커버리지 (code coverage) 개념에 대해서 살펴보자. 앞에서도 언급했지만 구조 기반의 테스트 케이스 디자인의 핵심은 코드 구조의 모든 경로 (path)를 실행할 수 있는 테스트 케이스를 만들어 내는 것이다. 특히나 코드 구조에서 논리식의 평가 결과에 따라서 이동하는 경로 (path)가 달라지므로 논리식이 가질 수 있는 모든 조합을 확인하는 것이 중요하며 결국 이것이 코드가 수행될 수 있는 모든 경로를 커버하는 것이다.
이러한, 코드 커버리지 정도 (level)에는 낮은 수준의 커버 정도에서 높은 수준의 커버 정도까지 다양한데, 이러한 여러 종류의 커버리지를 알아보자.
Statement coverage |
정의상 이 커버리지는 코드 구조내의 모든 구문 (statement)을 수행할 수 있다면 조건이 충족되는 것으로 본다. 예를 들어, IF A THEN B ELSE C의 구조라면 A가 TRUE의 값만을 가져서 B를 수행할 수 있어서 IF 구문이 실행되면 만족한다는 의미이다. 가장 낮은 수준의 커버리지이며 불완전하기 때문에 거의 실용적이지 않은 것으로 알려져 있다. (1) (하지만, 테스팅 완료 통과조건으로 처음 코드 커버리지를 사용하는 조직이라면 가장 먼저 시도하는 조건으로 추천된다.) |
Decision coverage (=Branch coverage) |
정의상 이 커버리지는 코드 구조내의 모든 분기 (decision)가 TRUE와 FALSE인 경우를 각각 수행할 수 있다면 조건이 충족되는 것으로 본다. (이하 T/F로 표기) 예를 들어, IF A THEN B ELSE C의 구조라면 A가 T의 값을 가져서 B를 수행할 수 있고, A가 F의 값을 가져서 C를 수행할 수 있다면 만족한다는 의미이다. |
Condition coverage |
정의상 이 커버리지는 코드 구조내의 모든 분기 (decision) 내의 각각의 단위 조건식이 TRUE와 FALSE를 가지는 경우를 각각 수행할 수 있다면 조건이 충족되는 것으로 본다. 예를 들어, IF (A and B) THEN B ELSE C의 구조라면 A, B가 T/F를 가지는 조건과 A, B가 F/T를 가지도록 수행할 수 있다면 만족한다는 의미이다. (이 반대도 허용된다.) 여기서 주의할 점은 개개의 조건이 T/F를 가질 때 전체적으로 분기 (decision)가 평가되어 나온 결과에 대해서는 신경을 쓰지 않는다는 점이다. 즉, 개개의 단위 조건이 T와 F를 모두 가지기만 하면 된다. 결국, 위의 예에서 (A and B)가 평가되어 나온 결과가 T 또는 F를 가지던 신경 쓰지 않고 평가되는 대로 수행하면 된다. |
Decision/Condition coverage |
정의상 이 커버리지는 코드 구조내의 모든 분기 (decision)에 대해서 decision coverage를 만족하면서 동시에 Condition coverage를 만족하면 조건이 충족되는 것으로 본다. 예를 들어, IF (A and B) THEN B ELSE C의 구조라면 A와 B가 T/T, F/F인 경우 만족한다는 의미이다. 이는 Decision coverage와 Condition coverage의 정의를 살펴보면 이해하기 쉬운데, Decision coverage는 분기를 평가한 결과가 T와 F를 가지면 만족하고, Condition coverage는 분기 내의 개개의 조건이 T와 F를 한 번씩만 가지면 만족하기 때문이다. 즉, T/T, F/F 조합은 이 조건과 일치한다. |
Modified Condition Decision/Condition coverage |
줄여서 MC/DC로 불리는 이 커버리지 수준은 정의상 분기 (branch)내에서 발생할 수 있는 모든 논리적인 조합 중에 단위 조건 하나를 제외한 모든 단위 조건들의 값이 고정된 상태에서 단 하나의 단위 조건의 값이 T 또는 F로 변경되면 전체 분기를 평가한 결과에 영향을 주는 조합 만을 추려서 중복을 제거하는 것이다. (A and B) 조건의 경우 이를 만족하는 조합은 T/T, T/F, F/T 이다. (F/F는 제거된다.) 이를 산출하는 방법은 이후에 소개하겠다. 테스트의 완전성을 위해서는 다음에 소개되는 Multiple Condition coverage를 만족하는 것이 적합하지만, 대단히 많은 수의 테스트 케이스가 생성될 수 밖에 없으므로 Multiple Condition coverage와 비슷한 수준의 결함 검출 능력을 가지면서 테스트 케이스의 수는 적게 산출하는 효과가 있다. |
Multiple Condition |
Loop 구조를 제외하면 가장 높은 커버리지 수준을 보장하며, 분기 내에서 발생할 수 있는 모든 논리적인 조합을 시험하는 조건이다. 일반적으로 높은 커버리지 수준을 보장하지만, 이에 근거한 테스트 케이스의 수는 2의 n 제곱 개의 수를 가지므로 이 커버리지를 만족하려면 현실적으로 대단히 많은 테스트 케이스를 필요로 한다. |
이제 CFG와 논리식의 계산을 이용해 이러한 커버리지를 좀 더 자세히 알아보도록 하자.
예를 들기 위해, (A and B and C) 조건이 주어졌다고 가정하며, 이를 CFG로 나타내면 다음과 같다.
그림 4. (A and B and C)가 포함된 Control flow graph
Statement coverage: 일반적인 CFG에서는 분기 (branch)에서의 TRUE를 우측 방향의 화살표 (edge, 간선)로 표현한다. 따라서, (A and B and C)가 TRUE로 평가되어 a – b 경로를 수행할 수 있는 A, B, C의 값이 statement coverage를 만족한다고 본다. 이 커버리지를 만족하는 값은 T/T/T만 존재한다.
Decision coverage: (A and B and C)의 전체적인 평가 결과가 TRUE와 FALSE를 각각 가지면 만족하므로, 이 커버리지를 만족하는 값은 T/T/T와 (각 조건 중 하나라도 F가 들어 있는 값)의 두 개의 케이스가 된다. 이는 AND 연산의 속성에 기인한 것인데 단위 조건의 하나라도 F를 가지면 그 전체 평가 결과는 F가 되기 때문이다.
Condition coverage: (A and B and C)의 단위 조건이 T와 F를 하나씩 가지는 조합이면 만족하므로, 이 커버리지를 만족하는 값은 T/T/T와 F/F/F의 두 가지가 된다.
Decision/Condition coverage: Decision가 Condition coverage를 모두 만족하면 되므로, 이 커버리지를 만족하는 값은 역시 T/T/T/와 F/F/F의 두 가지가 된다.
Modified Condition Decision coverage: 분기 (branch)에서 나머지 단위 조건의 값이 고정된 상태에서 각 단위 조건의 값이 변경될 때 그 전체 평가식의 결과에 영향을 받는 조합만을 대상으로 한다. 따라서, 이 커버리지를 만족하는 값은 T/T/T, T/T/F, T/F/T, F/T/T의 네 가지가 된다. 왜 이렇게 산출되는지는 이후에 이어지는 MC/DC 산출 방법 소개에서 다루도록 하겠다. 일반적으로 MC/DC의 값은 조건식 내부의 단위 조건의 (개수 + 1)의 수를 가진다. (A and B and C)의 예에서 산출할 수 있는 MC/DC를 만족하는 테스트 케이스의 개수는 4가 된다.
Multiple Condition coverage: 분기 (branch)에서 발생할 수 있는 모든 논리적인 조합을 시험할 수 있는 값을 가지면 만족하므로, 이 커버리지를 만족하는 값은 T/T/T, T/T/F, T/F/T, F/T/F, F/F/T, F/F/F, F/T/T, T/F/F의 여덟 가지가 된다.
그러면, 이러한 커버리지 조건들을 가지고 결함을 검출할 수 있는 능력을 살펴보자. 앞의 예에서와 같이 잘못 구현된 조건식을 각각의 커버리지를 통해 분석할 때 결함을 검출할 수 있는지를 살펴볼 것이다. 이러한 일련의 과정을 통해 MC/DC 커버리지 수준이 Multiple Condition 커버리지 수준의 합리적인 대안임을 알아볼 것이다.
조건 (A and B and C)에 대해서 (A or B or C)로 잘못 구현되었다고 가정하면 아래와 같이 결함 검출 및 미 검출이 가능할 것이다.
Statement coverage: 분기 (branch)가 TRUE만 가지면 만족하므로 T/T/T의 시험 값이 이 커버리지를 만족하지만, (T or T or T)의 경우도 T를 가지기 때문에 (A or B or C)로 잘못 구현된 결함을 발견할 수 없다.
Decision coverage: 분기 (branch)가 TRUE와 FALSE를 모두 가지면 만족하므로, T/T/T와 F/F/F가 이 커버리지를 만족하지만, (F or F or F)의 경우도 F를 가지기 때문에 (A or B or C)로 잘못 구현된 결함을 발견할 수 없다.
Decision/Condition coverage: 분기 (branch)가 TRUE와 FALSE를 모두 가지며, 단위 조건들이 T와 F를 가지면 만족하므로 T/T/T와 F/F/F가 이 커버리지를 만족하지만, 역시 (T or T or T)의 경우도 T를 가지기 때문에 (A and B and C)와 (A or B or C)의 차이를 구별해 낼 수 없다.
Modified Condition Decision coverage: 이 조건에 대해서는 MC/DC 커버리지로 산출된 개개의 테스트 케이스에 대해서 모두 결함 검출이 가능한지 다 따져보도록 하자. (Z는 평가한 최종 결과를 의미한다.)
① A = T, B = T, C = T이면 Z = T의 값을 가진다. 이 케이스로는 (A or B or C)로 잘못 구현된 결함을 발견할 수 없다.
② A = T, B = T, C = F이면 Z = F의 값을 가진다. 만일 (A or B or C)로 잘못 구현되었다면, Z = T의 값을 보일 것이므로 이 케이스로는 (A or B or C)로 잘못 구현된 결함을 발견할 수 있다!
③ A = T, B = F, C = T이면 Z = F의 값을 가진다. 마찬가지로, (A or B or C)로 잘못 구현되었다면, Z = T의 값을 보일 것이므로 이 케이스로는 (A or B or C)로 잘못 구현된 결함을 발견할 수 있다!
④ A = F, B = T, C = T이면 Z = F의 값을 가지므로 역시 잘못 구현된 결함을 발견할 수 있다!
만일 (A and (B or C))로 잘못 구현된 경우는 어떨까? 이 경우에도 MC/DC 커버리지를 만족하는 테스트 케이스들이 결함을 검출 할 수 있을까?
① T/T/T로는 결함이 검출 되지 않는다.
② T/T/F로는 Z = T가 되지만, (A and B and C)에서는 F가 나와야 하므로 결함 검출이 가능하다!
③ T/F/T로는 Z = T가 되지만, (A and B and C)에서는 F가 나와야 하므로 결함 검출이 가능하다!
④ F/T/T로는 Z = F가 되기 때문에 (A and B and C)의 결과와 같아져서 결함이 검출되지 않는다.
즉, 위의 결과로 볼 때 MC/DC가 Multiple Condition coverage 보다 생성된 테스트 케이스의 수는 적지만, 합리적인 수준의 결함 검출이 가능함을 알 수 있다. 그러면, 이러한 장점을 가진 MC/DC를 산출하는 방법에 대해서 알아보자.
MC/DC 산출 방법
n개의 동일 논리연산자(Logical Operators)로 구성된 논리식에서의 산출
AND나 OR 등의 한 가지의 논리 연산자로만 구성된 논리식의 경우 MC/DC 커버리지를 만족하는 최소한의 케이스는 다음과 같으므로 간단히 외우는 것이 좋다.
표 3. 한 개의 AND 조건에서 MC/DC 커버리지를 만족하는 조합
AND 조건 |
A |
B |
Z |
① |
T |
T |
T |
② |
T |
F |
F |
③ |
F |
T |
F |
|
|
|
|
표 4. 한 개의 OR 조건에서 MC/DC 커버리지를 만족하는 조합
OR 조건 |
A |
B |
Z |
|
|
|
|
② |
T |
F |
T |
③ |
F |
T |
T |
④ |
F |
F |
F |
AND의 경우 TT/TF/FT, OR의 경우 TF/FT/FF의 세 가지만 선택되었다. 이것이 MC/DC 커버리지를 만족하는 조합인 것이다. (AND 조건에서는 F/F를 버리고, OR 조건에서는 T/T를 버린다고 암기하는 것이 간편하다.)
이제 논리 연산자가 두 개인 경우를 살펴보자. (A and B and C)의 경우는 다음과 같은 최소한의 테스트 케이스가 MC/DC 커버리지를 만족하며, 이 조합을 유심히 보면 알 수 있듯이 MC/DC 커버리지를 만족하는 조합은 다음과 같은 패턴을 보인다.
표 5. 두 개의 AND 조건에서 MC/DC 커버리지를 만족하는 조합
AND 조건 |
A |
B |
C |
Z |
① |
T |
T |
T |
T |
② |
T |
T |
F |
F |
③ |
T |
F |
T |
F |
④ |
F |
T |
T |
F |
논리 연산자의 유형에 따라서 TRUE 또는 FALSE 값에 연산 결과가 결정적인 영향을 미치는 경우가 있는데 이러한 성질을 민감성 (Sensitivity)이라고 한다. 즉, AND 조건에는 FALSE가 민감성이 있고, OR 조건에는 TRUE가 민감성이 있다고 말할 수 있다.
그러므로, 다음과 같이 외우는 것이 간편하다. AND 조건에 민감성을 보이는 F가 결과값이 영향을 많이 미쳐서 Z 값에 F가 많기 때문에 TTT를 먼저 적고 나서, 민감성을 가지는 F를 우측부터 한 열씩 차례대로 써나간 후 비어있는 부분에 T를 채워 넣으면 최종적인 조합이 완성된다.
이것이 OR 조건에서도 적용될 수 있으며 다음과 같은 패턴에서 알 수 있듯이 OR 조건에 민감성을 보이는 T가 결과값에 영향을 많이 미쳐서 Z 값에 T가 많기 때문에 FFF를 먼저 적고 나서, 민감성을 가지는 T를 우측부터 한 열씩 차례대로 써나가고 비어있는 부분에 T를 채워 넣는 것이다.
표 6. 두 개의 OR 조건에서 MC/DC 커버리지를 만족하는 조합
OR 조건 |
A |
B |
C |
Z |
① |
F |
F |
F |
F |
② |
F |
F |
T |
T |
③ |
F |
T |
F |
T |
④ |
T |
F |
F |
T |
또 다른 논리연산자인 XOR, NOT 등에 대해서는 이 기고에서는 다루지 않는다. 좀 더 자세한 내용을 알고 싶다면 참고 문헌을(3) 참고하기 바란다.
여러 유형의 논리연산자(Logical Operators)가 뒤섞인 논리식에서의 산출
그러면, AND와 OR가 뒤섞인 논리식에서의 계산은 어떨까? 위에서 소개한 패턴을 보이지 않기 때문에 결과값과 각 입력의 논리를 일일이 따져서 산출해야 한다. (A or (B and C))의 진리표를 먼저 살펴보자.
표 7. A or (B and C)의 진리표
A or (B and C) |
A |
B |
C |
Z |
1 |
F |
F |
F |
F |
2 |
T |
F |
F |
T |
3 |
F |
T |
F |
F |
4 |
F |
F |
T |
F |
5 |
T |
T |
F |
T |
6 |
F |
T |
T |
T |
7 |
T |
F |
T |
T |
8 |
T |
T |
T |
T |
MC/DC 커버리지의 정의에 따라서 각각의 입력 값이 (A, B, C) 혼자서 결과에 영향을 미치며, 이때 다른 입력 값은 고정되어 있는 조합을 찾아보자.
먼저 B, C가 고정되어 있고 A가 변경됨에 따라 결과값이 변경되기 때문에 A가 독립적으로 결과에 영향을 미치는 조건이다.
표 8. A가 결과 Z에 독립적으로 영향을 미치는 조합
|
A |
B |
C |
Z |
A1 |
F |
F |
T |
F |
A2 |
T |
F |
T |
T |
이제 A, C가 고정되어 있고 B가 변경됨에 따라 결과값이 변경되는 조합은 다음과 같다.
표 9. B가 결과 Z에 독립적으로 영향을 미치는 조합
|
A |
B |
C |
Z |
B1 |
F |
F |
T |
F |
B2 |
F |
T |
T |
T |
마지막으로 A, B가 고정되어 있고 C가 변경됨에 따라 결과값이 변경되는 조합은 다음과 같다.
표 10. C가 결과 Z에 독립적으로 영향을 미치는 조합
|
A |
B |
C |
Z |
C1 |
F |
T |
T |
T |
C2 |
F |
T |
F |
F |
이제 모든 조합을 정리하면 다음과 같이 중복 제거가 가능하다.
A1 = B1, A2, B2 = C1, C2
따라서, 최종적으로 완성된 MC/DC 커버리지를 만족하는 최소의 조합은 다음과 같다.
표 11. 생성된 조합 간의 중복 관계
Case |
A |
B |
C |
Z |
1 (A1, B1) |
F |
F |
T |
F |
2 (A2) |
T |
F |
T |
T |
3 (B2, C1) |
F |
T |
T |
T |
4 (C2) |
F |
T |
F |
F |
산출된 결과만을 놓고 보면 정의에 맞게 산출되어 있지만, 왜 이렇게 산출되었는지에 대한 이해를 돕기 위해서 차례 차례로 다시 산출해 보자.
Unique cause와 Masking 개념
MC/DC 커버리지의 산출에 있어 입력 값 하나만이 다른 값에 상관없이 결과값에 영향을 미치는 경우를 unique cause라고 한다.
(논리 연산이 결과에 영향을 미치지 않기 때문에 X 값만 결과에 영향을 미치는 경우)
True and X => X
False or X => X
(논리 연산이 결과에 영향을 미쳐서 X 값이 가리워져 결과에 영향을 미치는 경우)
True or X => True
False and X => False
이것을 Masking이라고 부르며, 어떠한 값을 가지던 그 값이 다른 값에 가리워지는 효과를 말한다.
그러면, 이러한 기본 원리를 이해한 상태에서 위에서 언급한 예제에서 MC/DC를 만족하는 조합을 산출하는 방법을 다시 살펴보자.
먼저, A 값이 변경되면서 결과 값도 따라서 변하며, 동시에 B/C 값은 고정되는 조합을 찾는다. 제일 먼저 우리가 관심을 두는 값은 A이므로 A1/A2에 T/F를 써넣는다. 이후 Z값도 A값을 따라야 하므로 Z(A1/A2)에도 T/F를 써넣는다.
표 12. A이 독립적인 효과를 미치는 조합 산출하기
A or (B and C) |
A |
B |
C |
Z |
A1 |
T |
|
|
T |
A2 |
F |
|
|
F |
B/C에는 어떤 값이 들어가야 할까? 기억해야 할 중요한 사실은 B(A1/A2), C(A1/A2) 각각이 동일한 값을 가져야 한다는 점이다. A를 제외한 나머지는 A1/A2 모두 고정이어야 한다.
A2를 보자, A값에 이미 F가 들어 있다. A or (B and C)의 관계이므로 Z=F가 되려면 (B and C)를 평가한 결과는 반드시 F가 되어야 한다. 즉, A2(A)의 unique cause를 보장하려면 (B and C)를 평가한 결과는 무조건 F를 가져야 한다는 점이다. 이 시점에서 (B and C)가 가질 수 있는 조합에 대해서 살펴보자. T/F, F/T, F/F일 것이다. 하지만, F/F는 A/B/C 모두 F/F/F가 되어 버리기 때문에 이 값은 unique하지 않기 때문에 버린다. 이제 T/F와 F/T가 남았다. 이중 A가 독립적으로 결과 Z에 효과를 미치게 하려면, (B and C)는 무조건 F가 나오도록 Masking되어야 하므로, False and X => False의 Masking 효과를 사용해서, T/F 값은 버리고, F/T값을 넣는다. 최종적으로 완성된 A1/A2의 조합은 다음과 같다.
표 13. Unique cause와 Masking을 이용한 조합 산출
A or (B and C) |
A |
B |
C |
Z |
A1 |
T |
F |
T |
T |
A2 |
F |
F |
T |
F |
이제 B1/B2에 대해서 작성해 보자.
표 14. B가 독립적인 효과를 미치는 조합 산출하기
A or (B and C) |
A |
B |
C |
Z |
B1 |
|
T |
|
T |
B2 |
|
F |
|
F |
마찬가지로, B2(A) 값은 Z=F 이어야 하므로, A(B1/B2)는 F/F의 값을 가질 수 밖에 없다. 이번에는 간단하다. C(B1/B2)는 B1 때문에 당연히 T/T를 가져야 한다. 최종적으로 완성된 B1/B2의 조합은 다음과 같다.
표 15. 결과값 Z에 따라 논리상 적합한 조합 산출하기
A or (B and C) |
A |
B |
C |
Z |
B1 |
F |
T |
T |
T |
B2 |
F |
F |
T |
F |
이제 C1/C2에 대해서 작성해 보자.
표 16. C가 독립적인 효과를 미치는 조합 산출하기
A or (B and C) |
A |
B |
C |
Z |
C1 |
|
|
T |
T |
C2 |
|
|
F |
F |
역시 A(C1/C2)는 F/F를 가질 수 밖에 없다. 또한 C1 때문에 B(C1/C2)는 F/F의 값을 가질 수 없으며, T/T를 가진다. 최종적으로 완성된 조합은 다음과 같다.
표 17. 결과값 Z에 따라 논리상 적합한 조합 산출하기
A or (B and C) |
A |
B |
C |
Z |
C1 |
F |
T |
T |
T |
C2 |
F |
T |
F |
F |
결론적으로, 중복을 따져보면 A1, A2 = B2, B1 = C1, C3의 관계이므로 MC/DC 커버리지를 만족하는 최소한의 테스트 케이스의 수는 네 가지가 된다. 특히, 위에서 이해를 돕기 위해 AND로만 또는 OR로만 구성된 논리식의 MC/DC를 만족하는 최소한의 케이스 산출에 대해서도 이러한 식으로 차근차근히 써 내려 간다면 완성된 최종 결과가 왜 그렇게 나오게 되었는지에 대해서 이해할 수 있을 것이다.
결론
이 기고를 준비하면서 대단히 많은 참고 문헌을 참고했다. 용어의 생소함 뿐만 아니라 그 광범위함과 연결 관계 때문에 이 기고에서는 다루지 않은 부분이 너무나도 많다. 예를 들어, Control Flow Graph에 대해서도 McCabe Cyclomatic Complexity, 독립적인 경로를 산출하는 방법 (basis path testing) 등에 대해서도 알아야 할 부분이 많으며, 커버리지 툴을 사용한 테스트 전략, 컴파일러나 프로그래밍 언어의 Short-Circuit 회로 현상, Data Flow Testing 등의 고려 사항이 매우 많이 존재한다. 또한, MC/DC 커버리지의 경우에도 이러한 기본 개념을 학문적으로 다시 변형시킨 개념이 계속 나오고 있다. 나중에 필자의 역량이 쌓이게 된다면 이러한 부분에 대해서도 다시 한 번 여러 분들과 기고를 통해 나누고자 한다.
마지막으로 필자가 얘기하고 싶은 것은 구조 기반의 테스팅 접근 방법도 적용해 볼만 하지만, 이러한 여러 기법이 적용될 수 있는 분야에 제한을 두거나 고정시키지 말고 적극적으로 찾아보고 응용하자는 데 있다. 지금도 지구상의 어딘가에서는 다른 산업계나 다른 학문 분야의 이론들을 소프트웨어 테스팅에 접목하고 응용하려는 시도가 이루어 지고 있을 것으로 생각된다. 이러한 여러 노력들이 모여서 지금의 정형화된 테스팅 기법 및 접근 방법의 형태로 나타나게 되었을 것이다. 한 가지 아쉬운 점은 한국인의 손에 의해서 창안된 테스팅 기법에 대해서는 들어보지 못했다는 점이다. 무언가 없던 것을 만들어 내는 것도 대단한 일이지만 무언가의 개선을 위해서 여러 이론들을 접목해 보고 적용하는 일 또한 가치 있는 일이 될 것이다. 여러 분들도 이미 잘 알고 계시는 Orthogonal Array Testing이 사실은 제조업의 실험 기법을 차용한 것이라는 점은 이미 널리 알려져 있다. 아무쪼록 이 기고가 여러 분들의 테스팅 패러다임에 영향을 미쳐서 현재의 접근 방법에 대해서 다시 한 번 재고하게 만드는 기회가 되었으면 한다.
참고문헌
Myers, G. J., The Art of Software Testing, John Wiley and Sons, 1979
Grady, R. B., Successful Software Process Improvement, Prentice Hall, 1997
Kelly Hayhurst, et al., A Practical Tutorial on Modified Condition/Decision Coverage, 2001
Craig, R. D., and S. P. Jaskiel, Systematic Software Testing, Artech House, 2002
Copeland, A Practitioner’s Guide to Software Test Design, Artech House, 2004
[BS 7925-2] British Standard 7925-2, Software testing, Part 2; Software component, 1998
Martin Pol, Ruud Teunissen, Erik van Veenendaal, Software Testing: A guide to the TMap Approach, Addison-Wesley Professional, 2001
Erik van Veenendaal, et al., The Testing Practitioner, UTN Publishers, 2002
Paul Ammann, Jeff Offutt and Hong Huang, Coverage Criteria for Logical Expressions, 2003 International Symposium on Software Reliability Engineering (ISSRE '03). pages 99-107, November 2003
소프트웨어 공학 5판 (SOFTWARE ENGINEERING A PRACTITIONER'S APPROACH), Roger S. Pressman, 한국맥그로힐, 2001
- 끝 -
댓글 영역