데이터 흐름 테스팅

DoTheTest·2025년 8월 14일
0

테스트 지식

목록 보기
22/24

우리는 코드의 모든 실행 경로를 검증하기 위해 분기 커버리지나 조건 커버리지 같은 제어 흐름(Control Flow) 테스트에 많은 노력을 기울입니다. 하지만 코드의 '길'을 완벽하게 테스트했다고 해서, 그 길 위를 달리는 '데이터'의 여정까지 완벽하다고 보장할 수 있을까요?

  • 값이 할당되지 않은 변수를 사용하는 문제 (ud anomaly)
  • 값을 할당했지만, 한 번도 사용되지 않고 사라지는 비효율적인 코드 (dk anomaly)

이러한 '데이터의 비정상적인 생명주기'와 관련된 결함을 찾아내는 정밀한 화이트박스 테스트 기법이 바로 데이터 흐름 테스팅(Data Flow Testing)입니다.

1. 데이터 흐름 테스팅이란 무엇인가?

데이터 흐름 테스팅은 프로그램 내에서 변수의 생명주기(정의, 사용, 소멸)를 추적하고, 그 과정에서 발생할 수 있는 잠재적인 이상 현상을 검증하는 화이트박스 테스트 기법입니다.

  • 핵심 철학: 코드의 실행 경로(제어 흐름)뿐만 아니라, 그 경로를 따라 흐르는 데이터의 상태 변화(데이터 흐름)에 집중합니다.
  • 관점: 변수가 '어디서 태어나서(정의), 어떻게 사용되고(사용), 어디서 사라지는가(소멸)'를 중심으로 테스트 케이스를 설계합니다.

2. 데이터 흐름의 핵심 개념: Def, Use, Kill

데이터 흐름 테스팅을 이해하기 위해서는 변수의 세 가지 핵심 상태를 알아야 합니다.

  1. 정의 (Definition / def): 변수에 값이 할당되는 모든 지점.
  2. 사용 (Use / use): 변수의 값이 참조되는 모든 지점.
    • 계산용 사용 (Computation Use / c-use): 변수가 계산식이나 출력에 사용되는 경우 (예: y = x * 2;).
    • 술어용 사용 (Predicate Use / p-use): 변수가 if, while 문과 같은 조건문에서 실행 경로를 결정하는 데 사용되는 경우 (예: if (x > 10)).
  3. 소멸 (Kill / Un-define): 변수가 범위를 벗어나 유효하지 않게 되는 지점.

3. 데이터 흐름 테스팅의 커버리지 기준

데이터 흐름 테스팅은 여러 수준의 커버리지 목표를 가집니다. 일반적으로 다음 세 단계로 강도가 높아집니다.

  • All-Defs (모든 정의 커버리지): 가장 기본적인 수준. 프로그램 내의 모든 변수 정의(def) 지점에서, 그 변수가 사용되는 사용(use) 지점까지 이어지는 정의-클리어 경로(Definition-clear Path)가 최소 하나 이상 존재하도록 테스트합니다.

    정의-클리어 경로란? 변수가 특정 지점에서 정의(def)된 후, 다른 지점에서 사용(use)될 때까지 그 사이에 해당 변수가 재정의되지 않는 경로를 의미합니다.

  • All-Uses (모든 사용 커버리지): 더 강력한 기준. 모든 변수 정의(def)에 대해, 그 정의로부터 도달 가능한 모든 사용(use) 지점(모든 c-usep-use)을 커버하는 정의-클리어 경로를 테스트합니다.

  • All-du-paths (모든 정의-사용 경로 커버리지): 가장 강력한 기준. 모든 정의-사용 쌍(def-use pair) 사이에 존재하는 모든 정의-클리어 경로를 테스트합니다. 루프 등으로 인해 경로가 매우 많아질 수 있어, 실무에서는 비용 문제로 잘 사용되지 않습니다.

💡 비실행 경로(Infeasible Path)에 대한 주의
이론적으로는 존재하지만, 코드의 제약 조건 때문에 실제로는 실행이 불가능한 경로가 있을 수 있습니다. 커버리지 목표를 설정할 때 이러한 비실행 경로는 제외해야 합니다.

4. 실전 예제: 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)
  • 테스트 케이스 도출:

    1. TC1: a = 10, b = 0
      • x11이 되어 if문이 true. y11로 재정의. 최종 y11 출력.
      • 커버하는 경로: x(L1,L3), x(L1,L4), b(param,L3), b(param,L4), y(L4,L5)
    2. TC2: a = 5, b = 1
      • x6이 되어 if문이 false. L4는 실행 안 됨. 최종 y1 출력.
      • 커버하는 경로: x(L1,L3), b(param,L3), y(L2,L5)

이 두 테스트 케이스를 통해 모든 def-use 쌍을 최소 한 번씩 커버하여 All-Uses 커버리지를 만족시켰습니다.

💡 단락 평가와 p-use
if (x > 10 && b == 0)에서 x > 10false이면, b == 0평가조차 되지 않습니다(단락 평가). 따라서 bp-use를 제대로 테스트하려면, x > 10true로 만드는 테스트 데이터(a=10)를 설계하여 b가 실제로 평가되도록 만들어야 합니다.

5. 정적 분석 vs. 동적 테스트

  • 정적 분석 (Static Analysis): 컴파일러나 SonarQube 같은 도구는 명백한 데이터 흐름 이상 현상(Anomaly)을 코드를 실행하지 않고도 잘 찾아냅니다.
    • du (def-undef): 정의되지 않은 변수 사용 (초기화되지 않은 변수)
    • dd (def-def): 사용되지 않고 재정의되는 변수
    • dk (def-kill): 사용되지 않고 소멸되는 변수
  • 동적 테스트 (Dynamic Testing): 하지만 특정 런타임 경로에서만 발생하는 미묘한 데이터 관련 결함은 정적 분석만으로는 부족합니다. 데이터 흐름 테스팅 기법을 바탕으로 설계된 테스트 케이스를 실제로 실행하여 검증하는 동적 테스트가 반드시 병행되어야 합니다.

6. 결론: 제어 흐름을 넘어 데이터의 건전성까지

코드 커버리지를 높이는 제어 흐름 테스팅이 프로그램의 '도로망'을 검증하는 것이라면, 데이터 흐름 테스팅은 그 도로 위를 달리는 '자동차(데이터)' 자체의 상태와 경로가 올바른지를 검증하는 것과 같습니다.

모든 프로젝트에서 수동으로 데이터 흐름 테스팅을 수행하는 것은 비효율적일 수 있습니다. 하지만 이 개념을 이해함으로써, 우리는 변수의 생명주기를 더 신중하게 다루게 되고, 정적 분석 도구가 보고하는 경고의 의미를 더 깊이 있게 파악할 수 있습니다. 특히, 인터프로시저럴(함수 간) 데이터 흐름, 별칭(aliasing), 가변 객체 상태와 같은 심화 주제는 데이터 흐름 테스트의 난이도를 높이는 주범이므로 항상 주의를 기울여야 합니다.

궁극적으로, 견고한 소프트웨어는 올바른 실행 흐름과 건전한 데이터 흐름이라는 두 개의 바퀴가 함께 굴러갈 때 만들어집니다.


0개의 댓글