우리는 코드의 모든 실행 경로를 검증하기 위해 분기 커버리지나 조건 커버리지 같은 제어 흐름(Control Flow) 테스트에 많은 노력을 기울입니다. 하지만 코드의 '길'을 완벽하게 테스트했다고 해서, 그 길 위를 달리는 '데이터'의 여정까지 완벽하다고 보장할 수 있을까요?
ud
anomaly)dk
anomaly)이러한 '데이터의 비정상적인 생명주기'와 관련된 결함을 찾아내는 정밀한 화이트박스 테스트 기법이 바로 데이터 흐름 테스팅(Data Flow Testing)입니다.
데이터 흐름 테스팅은 프로그램 내에서 변수의 생명주기(정의, 사용, 소멸)를 추적하고, 그 과정에서 발생할 수 있는 잠재적인 이상 현상을 검증하는 화이트박스 테스트 기법입니다.
데이터 흐름 테스팅을 이해하기 위해서는 변수의 세 가지 핵심 상태를 알아야 합니다.
def
): 변수에 값이 할당되는 모든 지점.use
): 변수의 값이 참조되는 모든 지점.c-use
): 변수가 계산식이나 출력에 사용되는 경우 (예: y = x * 2;
).p-use
): 변수가 if
, while
문과 같은 조건문에서 실행 경로를 결정하는 데 사용되는 경우 (예: if (x > 10)
).데이터 흐름 테스팅은 여러 수준의 커버리지 목표를 가집니다. 일반적으로 다음 세 단계로 강도가 높아집니다.
All-Defs (모든 정의 커버리지): 가장 기본적인 수준. 프로그램 내의 모든 변수 정의(def
) 지점에서, 그 변수가 사용되는 사용(use
) 지점까지 이어지는 정의-클리어 경로(Definition-clear Path)가 최소 하나 이상 존재하도록 테스트합니다.
정의-클리어 경로란? 변수가 특정 지점에서 정의(
def
)된 후, 다른 지점에서 사용(use
)될 때까지 그 사이에 해당 변수가 재정의되지 않는 경로를 의미합니다.
All-Uses (모든 사용 커버리지): 더 강력한 기준. 모든 변수 정의(def
)에 대해, 그 정의로부터 도달 가능한 모든 사용(use
) 지점(모든 c-use
와 p-use
)을 커버하는 정의-클리어 경로를 테스트합니다.
All-du-paths (모든 정의-사용 경로 커버리지): 가장 강력한 기준. 모든 정의-사용 쌍(def-use
pair) 사이에 존재하는 모든 정의-클리어 경로를 테스트합니다. 루프 등으로 인해 경로가 매우 많아질 수 있어, 실무에서는 비용 문제로 잘 사용되지 않습니다.
💡 비실행 경로(Infeasible Path)에 대한 주의
이론적으로는 존재하지만, 코드의 제약 조건 때문에 실제로는 실행이 불가능한 경로가 있을 수 있습니다. 커버리지 목표를 설정할 때 이러한 비실행 경로는 제외해야 합니다.
All-Uses
커버리지 달성하기public void process(int a, int b) {
int x = a + 1; // L1: x 정의 (def)
int y = b; // L2: y 정의 (def)
if (x > 10 && b == 0) { // L3: x 사용 (p-use), b 사용 (p-use)
y = x + b; // L4: x 사용 (c-use), b 사용 (c-use), y 재정의 (def)
}
System.out.println(y); // L5: y 사용 (c-use)
}
All-Uses
커버리지를 달성하기 위한 테스트 케이스 설계:
def-use
쌍 식별:
x
: (L1, L3), (L1, L4)y
: (L2, L5), (L4, L5)b
: (파라미터, L3), (파라미터, L4)테스트 케이스 도출:
a = 10
, b = 0
x
는 11
이 되어 if
문이 true
. y
는 11
로 재정의. 최종 y
는 11
출력.x(L1,L3)
, x(L1,L4)
, b(param,L3)
, b(param,L4)
, y(L4,L5)
a = 5
, b = 1
x
는 6
이 되어 if
문이 false
. L4는 실행 안 됨. 최종 y
는 1
출력.x(L1,L3)
, b(param,L3)
, y(L2,L5)
이 두 테스트 케이스를 통해 모든 def-use
쌍을 최소 한 번씩 커버하여 All-Uses
커버리지를 만족시켰습니다.
💡 단락 평가와 p-use
if (x > 10 && b == 0)
에서x > 10
이false
이면,b == 0
은 평가조차 되지 않습니다(단락 평가). 따라서b
의p-use
를 제대로 테스트하려면,x > 10
을true
로 만드는 테스트 데이터(a=10
)를 설계하여b
가 실제로 평가되도록 만들어야 합니다.
du (def-undef)
: 정의되지 않은 변수 사용 (초기화되지 않은 변수)dd (def-def)
: 사용되지 않고 재정의되는 변수dk (def-kill)
: 사용되지 않고 소멸되는 변수코드 커버리지를 높이는 제어 흐름 테스팅이 프로그램의 '도로망'을 검증하는 것이라면, 데이터 흐름 테스팅은 그 도로 위를 달리는 '자동차(데이터)' 자체의 상태와 경로가 올바른지를 검증하는 것과 같습니다.
모든 프로젝트에서 수동으로 데이터 흐름 테스팅을 수행하는 것은 비효율적일 수 있습니다. 하지만 이 개념을 이해함으로써, 우리는 변수의 생명주기를 더 신중하게 다루게 되고, 정적 분석 도구가 보고하는 경고의 의미를 더 깊이 있게 파악할 수 있습니다. 특히, 인터프로시저럴(함수 간) 데이터 흐름, 별칭(aliasing), 가변 객체 상태와 같은 심화 주제는 데이터 흐름 테스트의 난이도를 높이는 주범이므로 항상 주의를 기울여야 합니다.
궁극적으로, 견고한 소프트웨어는 올바른 실행 흐름과 건전한 데이터 흐름이라는 두 개의 바퀴가 함께 굴러갈 때 만들어집니다.