뮤테이션 테스팅(Mutation Testing)
- 결함 기반 테스팅 기법 중 하나임(a fault based testing technique)
- 기 작성된 테스트 케이스들의 “테스트 적정성(test adequacy)”을 평가하는 기법
- 프로그램 소스 코드의 한 부분을 약간 변경하여 의도적으로 오류가 있는(즉, 원본과 약간 달라진) 프로그램 버전을 만들고, 기 준비된 테스트 케이스로 원본 프로그램과 변형된 프로그램 버전들을 함께 실행하여, 현 테스트 케이스가 원본 프로그램과 변형된 버전들을 구별(즉, 프로그램에 의도적으로 삽입된 오류들을 식별)해 낼 수 있는지 확인한다.
뮤테이션 테스팅 절차
1) 원본 프로그램을 일정한 변경 규칙에 따라 변경하여 여러 변형된 프로그램 버전을 생성한다. 원본 프로그램의 구문(syntax)을 변경하는 사전 정의된 변경 규칙은 ‘뮤테이션 오퍼레이터(Mutation operator)’라고 하며, 뮤테이션 오퍼레이터를 적용하여 생성된 원본과 약간 달라진 프로그램 버전을 ‘뮤턴트(mutant)’라고 부른다. 원래의 소스 코드가 개발자가 의도한 정확한 버전의 프로그램이라 가정했을 때 각 뮤턴트는 잘못 프로그래밍된 오류 버전(가상의 결함이 포함되어 있는 프로그램)을 나타낸다.
위 그림처럼 적용하는 뮤테이션 오퍼레이터의 수와 타입에 따라 하나의 원본 프로그램에 대해 무수히 많은 수의 뮤턴트가 생성된다. 각 뮤턴트는 한번에 단 한 개의 에러만을 포함하고 있다.
2) 기 준비된 일련의 테스트 케이스 집합(테스터가 주관적으로 도출했거나 또는 타 테스트 케이스 도출 기법을 적용하여 도출한 테스트 케이스들)으로 원본 프로그램과 뮤턴트를 실행하여 실행 결과를 비교한다.
주의: 기 준비된 테스트 케이스들이 원본 프로그램 자체를 제대로 실행하지 못하거나 그 결과가 부정확하거나 하는 문제가 있으면 안 된다. 뮤턴트를 실행 하기에 앞서 이런 문제가 해결된 테스트 케이스들이 준비되어 있어야 한다.
3) 뮤턴트의 실행 결과가 원본 프로그램의 실행 결과와 다르면 현 테스트 케이스로 뮤턴트 프로그램을 구별해 낼 수 있다는 의미이다(즉, 해당 뮤턴트에 삽입된 특정 오류를 현 테스트 케이스가 식별 해 낼 수 있다). 이 경우에 해당 뮤턴트 프로그램은 ‘죽은(killed)’ 것으로 처리되어 관심 대상에서 정상적으로 빠지게 된다.
4) 기 준비된 모든 테스트 케이스에서 뮤턴트의 실행 결과가 원본 프로그램의 실행 결과와 동일하면 해당 뮤턴트를 ‘라이브 뮤턴트(live mutant)’라고 부른다. 라이브 뮤턴트는 현 테스트 케이스 집합이 해당 뮤턴트를 kill 하기에 충분하지 않다는 것을 나타내므로(예를 들어, 기 생성된 모든 테스트 케이스가 뮤턴트의 코드가 달라진 부분에 아예 접근하지 않음), 라이브 뮤턴트를 kill 할 수 있는 신규 테스트 케이스를 추가로 작성하여 다시 실행을 반복한다.
뮤테이션 오퍼레이터(Mutation operators)
의도적으로 변형된 프로그램인 뮤턴트는 프로그래머가 범하기 쉬운 구문 에러(typical syntactic errors)를 흉내 낸다. 이런 뮤턴트들은 사전 정의된 일련의 뮤테이션 오퍼레이터를 원본 소스 코드에 적용함으로써 자동 생성된다. 즉, 뮤테이션 오퍼레이터는 소스 코드의 syntax 변경 규칙이다. 다양한 종류의 뮤테이션 오퍼레이터가 연구를 통해 제안되었으며, 원본 프로그램이 어떤 프로그래밍 언어로 작성되었는지에 따라 적용하는 뮤테이션 오퍼레이터도 달라진다. 아래는 초기 뮤테이션 테스팅 연구가 집중되었던 Fortran 언어에 적용하는 22개의 뮤테이션 오퍼레이터 셋의 일부이다.
뮤테이션 오퍼레이터 | 설명 | 예 |
ABS(Absolute Value Insertion) | 각 산술 연산식에 abs(), negAbs(), failOnZero()를 추가. abs는 연산식의 절대값을 계산하고, negAbs는 음수 절대값을 계산함. failOnZero는 연산식 값이 0인지 테스트 함 | x=3*a à x=3*abs(a) |
AOR(Arithmetic Operator Replacement) | 연산 오퍼레이터(예, +, -, *, /, **, MOD)를 다른 적법한 연산 오퍼레이터로 대체함 | a=b+c à a=b-c |
CRP(Constant Replacement) | 각 상수 값을 같은 범위 내에 다른 상수로 변경 | a=5; b=3; à a=3; b=3; |
LCR(Logical Connector Replacement) | 각 논리 연산자(예, AND, OR, XOR)를 다른 적법한 논리 연산자로 대체 | a=b&c à a=b|c |
ROR(Relational Operator Replacement) | 각 관계 연산자(예, <, <=, >, >=, ==, !=)를 다른 적법한 관계 연산자로 대체 | while(a<b) à while(a>b) |
SDL(Statement Deletion) | 프로그램 코드의 문장 삭제(각 문장을 CONTINUE로 대체), 단, IF, ELSE IF, END IF 문 등은 대체 안함 | if else 구문에서 else만 삭제 |
SVR(Scalar Variable Replacement) | 각 스칼라 변수를 같은 범위(scope) 내 다른 스칼라 변수로 대체(같은 타입 또는 호환이 가능한 타입을 가진 변수끼리 교환) | x=a*b à x=a*a |
UOI(Unary Operator Insertion) | 단항 연산자를 삽입. 각 산술식이 부정으로 바뀌거나, 1 증가되거나, 1 감소된다. 각 논리식을 보수화 한다. | a=3*b à a=3*-b |
Fortran, C 같은 순차 언어 외에 객체 지향 언어나 기타 non-imperative 언어가 등장함에 따라 이에 적합한 뮤테이션 오퍼레이터들도 연구되었다. 또한 프로그래밍 언어뿐만 아니라 요구사항 및 상위 레벨 명세 언어에도 뮤테이션 기법을 적용하기 위해 뮤테이션 오퍼레이터를 고안하는 연구도 진행되고 있다.
동등 뮤턴트(Equivalent mutants)
라이브 뮤턴트 중에는 아무리 테스트 케이스를 추가해도 오리지널 프로그램과 구별이 불가능한(즉, 항상 동일한 실행 결과가 나오는) 것들이 존재한다. 예를 들면 아래 제시한 뮤턴트는 어떤 테스트 케이스로 실행 시켜도 원본 프로그램과 항상 동일한 결과를 낸다. 이렇게 소스 코드 구문상으로는 차이가 있지만 기능적으로 차이가 없는 프로그램 변형들을 ‘동등 뮤턴트(Equivalent mutant)’라고 하며, 이게 테스트 적정성 평가에 기여하지 않으므로 뮤턴트 목록에서 빠져야 한다.
특정 라이브 뮤턴트가 동등 뮤턴트인지 아닌지는 자동으로 확인이 불가능하고 사람이 일일이 확인해야 하므로 많은 시간과 노력이 요구된다. 따라서 동등 뮤턴트는 실무에서 뮤테이션 테스팅을 적용하는데 있어 큰 제약사항이다(동등 뮤턴트 자동 식별을 위한 여러 연구가 진행되고 있지만 근본적으로 불확정성을 가진 문제이므로 완전한 해결은 어려운 듯).
뮤테이션 테스팅의 이론적 가정(Hypothesis)
뮤테이션 테스팅은 프로그래머가 할 수 있는 실수 중 아주 사소한 코드 구문 오류(small syntactical changes)를 타겟으로 하고 있는데, 프로그램 코드에서 하이픈(-) 하나를 빼먹어 로켓이 폭발하는 등의 사고가 실제로 발생하는데 그 기원을 두고 있다. 뮤테이션 테스팅의 효과성은 아래와 같은 두 가지 가정을 전제로 한다.
- 유능한 프로그래머 가정(The Competent Programmer Hypothesis): 프로그래머가 혹 실수를 한다 하더라도 요구되는 것과 비슷한 프로그램을 만들지 완전히 다른 엉뚱한 프로그램을 생성하지는 않는다. 즉, 프로그래머가 작성한 프로그램이 정확한 버전(a correct program)은 아닐지라도 이 정확한 버전에 가까운 프로그램이다.
- 결합 효과(The Coupling Effect): 프로그램의 내부 요소들이 매우 조밀하게 얽혀 있으므로 단순 에러를 통해 더 복잡한 유형의 에러도 모습을 드러낸다. 다시 말해 뮤테이션 테스팅이 타겟으로 하는 단순 에러(simple errors)를 식별해 내는 테스트 케이스들은 이런 사소한 에러들이 얽혀 있는 더 복잡한 유형의 오류(complex errors)도 식별해 낼 수 있을 정도로 충분히 민감(sensitive)하다.
약한 뮤테이션 테스팅(Weak Mutation Testing)
일반적으로 테스트 케이스가 뮤턴트를 kill 하기 위해서는 아래 두 조건이 충족되어야 한다.
- 테스트 입력 데이터가 뮤턴트와 오리지널 프로그램이 각기 다른 프로그램 상태를 갖도록 야기해야 함
- 변경의 영향(부정확한 프로그램 상태)이 출력 데이터로까지 전파되고 관측되어야 함
테스트 케이스 실행으로 나온 출력값 비교를 통해 뮤턴트를 kill 하는 “전통적인 뮤테이션 테스팅(strong mutation testing)”은 위 두 조건이 모두 충족될 것을 요구하므로 컴퓨팅에 드는 시간과 비용이 높다. 이 문제를 개선하기 위해 제안된 “약한 뮤테이션 테스팅(weak mutation testing)”은 오로지 첫번째 조건의 충족 만을 요구하므로 훨씬 더 적은 컴퓨팅 파워를 필요로 한다. 즉, 약한 뮤테이션은 최종 결과(the final output)가 나올 때까지 기다리는 대신에 프로그램의 뮤테이션(변경) 된 부분이 실행된 후에 즉시 실행을 멈추고 오리지널 프로그램과 뮤턴트의 내부 상태(the internal states)를 비교한다. 약한 뮤테이션 테스팅이 고려해야 할 프로그램 뮤턴트의 수를 감소시키는건 아니지만 테스트 실행과 컴파일을 줄여서 성능 이점을 얻게 한다.
뮤테이션 테스팅의 활용
뮤테이션 테스팅 기법은 아래와 같은 다양한 목적으로 활용된다.
1) 테스트 케이스의 적정성/품질 평가
테스트 수행 시 고민 중 하나는 어느 정도 테스트를 해야 적정한지 여부이다. 대부분의 경우 완전한 테스팅(exhaustive testing)이 불가능하므로 일정 수준에서 테스트를 멈춰야 하는데, 테스트 수행 정도가 시스템 품질에 영향을 미치므로 테스트 비용(시간과 노력) 대비 품질의 적정 수준을 찾는 노력이 필요하다. 테스트 적정성 측정 기준(test adequacy criteria)은 얼마만큼 테스트를 하고 멈추어야 하는지 정량적으로 측정 가능한 기준을 제시해 준다. 여러 다양한 기준이 존재하는데 뮤테이션 기법도 테스트 적정성 기준의 하나로 활용된다. 기 준비된 일련의 테스트 케이스들로 뮤턴트들을 실행시켰을 때 식별이 되는 뮤턴트(killed mutants)들과 그렇지 않은 뮤턴트(live mutants)들이 있는데, 아래와 같은 뮤테이션 점수(Mutation adequacy score: MS)를 구하여 현재 준비된 테스트 케이스 집합의 적정성을 평가하는 것이다. 뮤테이션 점수(MS)가 높은 수록 양호한 테스트 케이스로 평가한다.
2) 신규 테스트 케이스 도출
뮤테이션 테스팅은 현재 준비된 테스트 케이스들이 어느 부분에서 취약한지 보여 주고 그 부분을 보완하는 테스트 케이스를 도출하도록 가이드하는 테스트 케이스 도출 기법(test case generation technique)의 역할을 한다. 뮤턴트는 특정 구문 에러(syntactical errors)를 의도적으로 삽입한 프로그램 버전이다. 이 뮤턴트들을 테스트 케이스로 실행했을 때 원본 프로그램과 구별하지 못하는 것은(즉, 실행 결과가 동일), 현재의 테스트 케이스들이 에러가 삽입된 코드 부분을 접근하지 않거나 또는 해당 부분을 접근하더라도 삽입된 에러를 식별해 내지 못하는 것을 암시한다. 이는 해당 코드 영역에 실제 유사한 버그가 있어도 현재의 테스트 케이스로는 찾아내지 못한다는 의미일 수 있다. 따라서 테스터는 라이브 뮤턴트(동등 뮤턴트 제외)를 kill 하기 위한 테스트 케이스들을 추가로 생성하여 테스트 케이스 집합의 품질(프로그램 커버리지 및 결함 식별 능력)을 올릴 수 있다.
3) 테스트 케이스 선별 기준(test case selection criteria)
테스트 케이스 각각을 실행하고 결과를 확인하는데 비용이 들어가므로 무조건 많은 수의 테스트 케이스를 만들어 테스트를 수행한다고 좋은 것은 아니다. 또한 프로젝트 사정상 도출된 테스트 케이스를 전부 실행하지 못하는 경우가 있으므로 테스트 케이스의 선별 문제가 생긴다. 예를 들어, 테스트를 여러 번 반복해야 하는 경우에 1차 시험에서는 100건의 테스트 케이스를 실행했지만 2차 시험에서는 시간/비용 제약으로 20건만 수행 가능하다면, 어떤 테스트 케이스를 골라야 테스트가 가장 효과적일지(결함도 최대한 많이 식별해 내고 프로그램의 커버 영역도 가장 넓은지)를 고민하게 된다. 여러 해결책이 있을 수 있지만 뮤테이션 기법이 이런 상황에서 테스트 케이스 선별 기준으로 활용 될 수 있다. 뮤턴트를 가장 많이 식별(kill)하는 테스트 케이스가 우선적으로 선택된다. 바꾸어 말해 뮤턴트를 식별하는데 전혀 도움이 안 되는 테스트 케이스가 우선적으로 테스트 케이스 집합에서 빠지게 된다.
NOTE: 어떤 테스트 케이스가 어떤 뮤턴트를 얼마나 kill 하는지를 담은 테스트 실행 정보가 뮤테이션 테스팅 도구를 통해 자동적으로 기록 및 관리된다는 가정에서 이런 활용이 가능함. 수작업으로 일일이 이런 정보를 확인하는 것은 거의 불가능하다.
뮤테이션 테스팅 단점
- 높은 컴퓨팅 비용(high computational cost): 생성된 무수히 많은 수의 뮤턴트를 테스트 스위트에 포함된 무수히 많은 테스트 케이스로 실행하고 결과를 확인하는데 많은 컴퓨팅 리소스가 소요되며 성능 문제도 발생함
- 높은 인적 비용(additional human effort): 동등 뮤턴트를 식별하거나, 원본 프로그램 실행 결과(이게 뮤턴트 실행 결과의 비교 기준이 됨)가 각 테스트 케이스마다 정확한지 확인하는 일에 사람의 개입이 필요함
뮤테이션 테스팅 History
- Richard Lipton이 1971년 제안
- Fortran IV 프로그램의 뮤테이션 테스팅 도구인 PIMS가 1977년 등장
- 1978년 DeMillo, Lipton, Sayward의 페이퍼에 의해 본격적으로 알려짐
- 70, 80년대에 단위 테스팅에 효과적인 테스트 기법으로 관심을 받았지만, 높은 컴퓨팅 비용으로 인해 실무에 널리 적용되지 못하고 점차 관심에서 사라짐
- 이후 강력한 컴퓨팅 파워를 가진 머신의 등장으로 다시 연구가 활발해짐