# 테스트 커버리지 분석 방법

#### 테스트 커버리지의 개념

테스트 커버리지(Test Coverage)란 소스코드에서 실제 테스트가 수행된 부분과 수행되지 않은 부분을 정량적으로 측정하는 지표다. 소프트웨어 품질을 관리하는 측면에서, 아래와 같은 이점을 제공한다.

* 테스트에서 누락된 영역 식별: 커버리지가 낮은 부분은 추후 발생할 수 있는 잠재적 결함의 위험이 높다.
* 테스트 전략 보강: 부족한 테스트 항목을 발굴하고, 테스트 설계를 더욱 체계적으로 계획할 수 있다.
* 유지보수 용이성 확보: 테스트가 충분히 수행된 코드 영역은 리팩터링과 같은 유지보수 작업에 대해 비교적 안전하다.

테스트 커버리지 분석을 수행하기 위해선, 컴파일 단계에서 추가적인 플래그 혹은 도구를 사용해 테스트 실행 중 어떤 부분의 코드가 실행되었는지 추적해야 한다. 일반적으로 커버리지 도구는 **기호화(Symbolization)**, **인스트루먼테이션(Instrumentation)**, \*\*트레이싱(Tracing)\*\*과 같은 메커니즘을 통해 실행 경로에 대한 정보를 수집한다.

#### 커버리지 측정 범주

소프트웨어 테스트 커버리지를 측정할 때 가장 널리 사용되는 유형은 크게 다음과 같다.

**라인(Line) 커버리지**: 테스트가 소스코드의 각 라인을 몇 번이나 실행했는지 측정한다. 이를 정량화하면 다음과 같은 형태로 표현할 수 있다.

$$
\text{Line Coverage} = \frac{\text{Number of executed lines}}{\text{Total lines of code}} \times 100%
$$

라인 커버리지는 가장 기초적이며, 구현 로직의 모든 라인에 대해 테스트가 어느 정도 수행되었는지 확인하는 데 유용하다.

**브랜치(Branch) 커버리지**: 조건문(if, switch 등)으로 인해 분기가 발생하는 모든 경로(분기)가 테스트에서 실행되었는지 측정한다. 예를 들어, if 조건문이 참일 때와 거짓일 때의 처리가 모두 테스트되었는지를 확인한다.

$$
\text{Branch Coverage}  = \frac{\text{Number of executed branches}}{\text{Total branches}} \times 100%
$$

브랜치 커버리지가 높을수록 분기 로직에서 발생할 수 있는 예외적인 상황을 놓칠 확률이 낮아진다.

**함수(Function) 커버리지**: 함수 또는 메서드 단위로 호출 여부를 측정한다. 모든 함수를 한 번 이상 호출했는지, 특정 파라미터 조합으로 충분히 테스트되었는지 등을 확인한다.

**조건/결정(Condition/Decision) 커버리지**: 단순히 분기가 나뉘었는지 여부만 보는 것이 아니라, 복합 분기(예: 복수의 조건이 결합된 if 문)에 대한 내부 조건들이 모두 테스트되었는지 확인하는 방식이다. 일반적인 브랜치 커버리지보다 측정이 더 복잡할 수 있지만, 더욱 정교한 커버리지 분석을 가능하게 한다.

#### ROS2 Humble에서의 커버리지 측정 개요

ROS2 Humble 환경에서 테스트 커버리지를 분석하기 위해서는 다음과 같은 과정을 고려할 수 있다.

* **빌드 시스템 구성**: ROS2의 공식 빌드 도구인 `colcon`을 사용하며, CMake 옵션 또는 컴파일러 플래그를 통해 커버리지 측정 기능을 활성화한다.
* **테스트 실행**: `colcon test`를 통해 빌드된 패키지에 대한 테스트를 수행한다. 이때, 커버리지 데이터를 수집할 수 있도록 빌드 및 실행 환경을 구성해야 한다.
* **보고서 생성**: 커버리지 데이터를 시각화할 수 있는 툴(예: `lcov`, `gcovr` 등)을 통해 최종적인 커버리지 리포트를 생성한다.

일반적인 워크플로우는 다음과 같다.

```
colcon build --cmake-args -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_FLAGS="--coverage" ...
colcon test --packages-select <패키지명>
```

테스트가 끝난 뒤, 생성된 커버리지 정보(예: `.gcda`, `.gcno` 등)를 기반으로 리포트를 생성한다. 예시는 다음과 같이 작성할 수 있다.

```
lcov --capture --directory . --output-file coverage.info
genhtml coverage.info --output-directory coverage_report
```

#### 커버리지 측정 도구와 활용

ROS2 Humble에서 C++ 및 Python 등 여러 언어가 혼합되어 쓰일 수 있으므로, 어떤 언어로 작성된 노드인지에 따라 측정 도구가 달라질 수 있다.

1. **GCOV & LCOV (C/C++용)**
   * **GCOV**: GCC(GNU Compiler Collection)에서 제공하는 커버리지 측정 도구로, 실행 파일을 인스트루먼테이션하여 `.gcno`와 `.gcda` 파일을 생성한다.
   * **LCOV**: GCOV의 결과물을 좀 더 보기 좋게 HTML 형태로 변환해주는 도구다.
2. **gcovr (C/C++용)**
   * GCOV 기반이지만, `gcovr` 명령어로 손쉽게 커버리지 레포트를 HTML이나 XML 형태로 만들 수 있다.
3. **Coverage.py, Coverage.pyxml (Python용)**
   * Python 코드를 대상으로 테스트 커버리지를 측정해주는 도구다.
   * `pytest-cov` 플러그인을 통해 손쉽게 통합할 수도 있다.

ROS2 프로젝트가 C++로 작성되어 있다면, `colcon` 명령어와 GCOV/LCOV 혹은 gcovr를 함께 사용하는 것이 대표적이다. 여기서는 C++ 예시를 위주로 설명하되, Python이 주를 이루는 패키지의 경우에도 유사한 프로세스를 따르며 적합한 커버리지 측정 도구를 선택하면 된다.

#### 빌드 플래그와 주의 사항

테스트 커버리지를 측정하기 위해 가장 먼저 고려해야 할 부분은 **컴파일 옵션**과 **링커 옵션**이다. ROS2 Humble 환경에서 C++ 코드를 기준으로 설명하면 다음과 같은 부분을 주의 깊게 살펴봐야 한다.

* **Debug 빌드 유형** 일반적으로 커버리지 수집을 위해서는 $-O0$(최적화 끔) 또는 $-Og$(디버깅 친화적 최적화) 모드로 컴파일하는 것이 권장된다. 최적화 레벨이 높으면 커버리지 데이터가 왜곡될 수 있다.
* **커버리지 전용 플래그**
  * `--coverage`: GCC 또는 Clang 컴파일러에서 사용된다. 내부적으로 `-fprofile-arcs`와 `-ftest-coverage`를 활성화한다.
  * `-fprofile-arcs`: 각 분기 지점에 대한 실행 횟수를 기록한다.
  * `-ftest-coverage`: 테스트 시나리오에 대해 추가적으로 커버리지 데이터를 생성한다.
* **정적/동적 링크 시 고려 사항** 공유 라이브러리를 사용하는 경우, 라이브러리 단에서 커버리지 플래그가 적용되지 않으면 전체적인 커버리지에 포함되지 않을 수 있다. 만약 특정 라이브러리도 테스트 범위에 포함시키려면, 해당 라이브러리 또한 같은 빌드 플래그로 빌드되어야 한다.
* **배포 빌드와 혼동** 실제 제품(런칭 시점)에서 사용하는 빌드 설정과 테스트 커버리지 빌드는 분리 관리하는 것이 좋다. 배포 빌드에서는 최적화를 적용해야 성능이나 용량 면에서 유리하지만, 커버리지 빌드에서는 테스트 범위 측정이 우선이므로 오버헤드가 더 발생해도 괜찮다.

아래는 예시로 `colcon build` 시 커버리지 옵션을 추가하는 방식이다.

```
colcon build \
  --cmake-args \
  -DCMAKE_BUILD_TYPE=Debug \
  -DCMAKE_CXX_FLAGS="--coverage -O0" \
  -DCMAKE_EXE_LINKER_FLAGS="--coverage"
```

#### CI(Continuous Integration)와 연동

개발 프로세스에서 커버리지 측정은 단발적인 분석보다는 **지속적 통합(CI)** 파이프라인에 포함시켜 자동화하는 편이 효율적이다. 예를 들어, GitHub Actions나 GitLab CI/CD, Jenkins 등 다양한 CI 환경에서 다음과 같은 단계를 자동화할 수 있다.

1. **빌드 스텝** 커버리지 플래그를 적용하여 소스코드를 빌드한다.
2. **테스트 스텝** `colcon test` 명령을 사용하여 모든 단위테스트와 통합테스트를 수행한다.
3. **커버리지 리포트 생성** GCOV, LCOV, gcovr 등을 이용해 `.gcda` 파일을 취합하고, 결과를 HTML 혹은 XML로 변환한다.
4. **리포트 업로드** CI에서 생성된 커버리지 보고서를 아티팩트(Artifacts) 또는 서드파티 서비스(Codecov, Coveralls 등)에 업로드해 시각화한다.

일례로 GitHub Actions의 YAML 구성에서 빌드, 테스트, 그리고 커버리지 리포트 생성을 스크립트 형태로 추가할 수 있다.

```yaml
name: CI

on:
  push:
    branches: [ "main" ]

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Setup ROS2
        # ROS2 설치 및 환경 설정
      - name: Build with coverage flags
        run: |
          colcon build \
            --cmake-args \
            -DCMAKE_BUILD_TYPE=Debug \
            -DCMAKE_CXX_FLAGS="--coverage -O0" \
            -DCMAKE_EXE_LINKER_FLAGS="--coverage"
      - name: Run tests
        run: colcon test --event-handlers console_direct+
      - name: Generate coverage report
        run: |
          sudo apt-get update && sudo apt-get install -y lcov
          lcov --capture --directory . --output-file coverage.info
          lcov --remove coverage.info "/usr/*" --output-file coverage.info
          genhtml coverage.info --output-directory coverage_report
      - name: Upload coverage artifact
        uses: actions/upload-artifact@v2
        with:
          name: coverage_report
          path: coverage_report
```

이와 같이 CI 환경에 커버리지 측정을 연동해두면, 풀 리퀘스트(merge request)가 생성될 때마다 자동으로 커버리지 결과를 확인할 수 있어 편리하다.

#### 커버리지 측정 결과 해석과 효율적 활용

테스트 커버리지 리포트를 생성한 뒤에는 **각 범주의 커버리지 지표**를 해석하고, 이를 바탕으로 **테스트 전략을 개선**하는 단계가 필수적이다. 일반적으로 HTML 보고서를 생성해 살펴보는 경우가 많으며, 각 파일별/함수별/라인별로 어느 정도 테스트되었는지 시각화가 가능하다.

* **라인(Line) 커버리지 지표 해석** 코드를 작성할 때 단순히 “커버리지 100%”에 집착하기보다는, 실제로 해당 라인을 테스트하는 로직이 충분히 존재하는지 판단해야 한다. 경우에 따라선 데드 코드(dead code)나 테스트 불가능한 코드를 발견하는 계기가 될 수 있다.
* **브랜치(Branch) 커버리지 지표 해석** 예외 처리나 에러 핸들링 구문이 제대로 커버되고 있는지 확인할 수 있는 유용한 지표다. 브랜치 커버리지가 낮다면 조건문이나 예외 처리 구간에 대한 테스트 케이스가 부족한지를 검토해야 한다.
* **함수(Function) 커버리지** 특정 함수가 전혀 호출되지 않는다면, 불필요한 코드인지 혹은 테스트 작성이 누락되었는지 고민할 수 있다.
* **조건/결정(Condition/Decision) 커버리지** 복합 조건에 대해 세밀한 테스트가 필요한 경우 유용하다. 간단한 예시로, if 문에 &&, ||와 같은 여러 조건이 결합되어 있다면 각각의 케이스가 제대로 검증되는지 파악 가능하다.

#### 커버리지 임계값(Threshold) 설정

현실적으로 모든 프로젝트에 대해 “커버리지 100%”를 달성하는 것은 과도한 비용이 들 수 있다. 따라서 프로젝트 규모, 중요도, 리스크를 고려하여 \*\*적절한 커버리지 임계값(Threshold)\*\*을 설정해 두는 것이 좋다.

* **예시**: “라인 커버리지 80%, 브랜치 커버리지 70% 이상은 충족해야 한다.”
* CI 파이프라인에서 이 임계값을 자동으로 체크하도록 구성하면, 커버리지가 기준 미달일 때 빌드가 실패(fail)하도록 처리할 수 있다.
* 단순 수치보다 더 중요한 것은 “어떤 부분이 누락되었는지”를 파악하고, “어떤 테스트 케이스를 추가해야 하는지” 결정하는 과정이다.

#### 다중 패키지/프로젝트 커버리지 병합

ROS2 프로젝트에서는 여러 패키지를 동시에 빌드하고 테스트하게 된다. 이 경우, **각 패키지마다 생성된 커버리지 데이터를 병합**하는 작업이 필요할 수 있다.

1. **패키지별로 `.gcda` 파일 수집** 각 빌드 디렉터리에서 테스트 실행 후 생성되는 `.gcda`, `.gcno` 파일을 별도로 모은다.
2. **병합 유틸리티 사용** LCOV의 경우 `lcov --add-tracefile` 옵션을 통해 서로 다른 정보 파일(coverage.info)을 하나로 결합할 수 있다. GCOVR 역시 유사한 방식으로 여러 디렉터리나 파일을 통합한다.
3. **최종 보고서 생성** 병합된 커버리지 정보를 바탕으로 최종 HTML 보고서를 만든다.

이 과정을 CI 스크립트나 별도의 쉘 스크립트로 자동화하면, 모든 패키지에 대한 테스트가 끝난 후 단일 보고서로 일괄 확인할 수 있어 훨씬 관리가 용이하다.

#### Python 기반 노드의 커버리지

ROS2 프로젝트에서 Python 노드를 사용한다면, `pytest` 혹은 `unittest`를 활용하여 테스트를 작성할 수 있다. 커버리지 분석을 위해선 다음과 같은 도구/방법을 고려할 수 있다.

* `coverage.py`
  * Python 전용 커버리지 측정 툴이다.
  * `pytest` 플러그인(예: `pytest-cov`)과 함께 사용하면, 테스트 실행과 동시에 커버리지 결과를 수집할 수 있다.
* 명령어 예시

  ```shell
  pip install coverage pytest-cov
  coverage run -m pytest
  coverage report -m
  ```

  또는

  ```shell
  pytest --cov=<패키지 경로> --cov-report=term-missing
  ```
* **ROS2와 연동** ROS2 파이썬 패키지(`rclpy`)를 사용해 작성한 노드들도 `colcon` 빌드 후 `colcon test`로 실행 가능하다. 다만, C++ 커버리지와는 별도의 측정 도구를 사용해야 하므로, CI 상에서 두 결과를 병행하거나 합산하는 과정이 필요할 수 있다.

#### 고급 커버리지 기법과 주의 사항

테스트 커버리지 수치는 소프트웨어 품질의 전부를 대변하지 않지만, **결함 발견률**을 높이고 **테스트 누락 영역**을 찾는 중요한 수단이다. 특정 프로젝트 상황(멀티쓰레드, 분산 환경, 실시간성 요구사항 등)에 따라 다음과 같은 **고급 기법**이나 **주의 사항**을 고려할 수 있다.

**멀티쓰레드 및 비동기 코드에서의 커버리지**

ROS2 노드 중에는 멀티쓰레드를 사용하거나 콜백 스핀(Callback spinning) 구조로 병렬 처리를 수행하는 코드가 많다. 비동기적으로 동작하는 코드의 커버리지를 확보하기 위해선 아래 사항을 신경 써야 한다.

* 테스트 시나리오 다양화
  * 다중 스레드가 동시에 특정 함수를 호출하거나 공유 리소스에 접근하는 상황을 다양하게 만들어야 한다.
  * 예: 콜백이 동시에 여러 개 등록된 경우, 각 콜백이 충돌 없이 잘 동작하는지 확인.
* 교착 상태(Deadlock) 테스트
  * 잠재적 교착 상태를 유발할 수 있는 락(lock) 사용 구간, 조건 변수를 사용하는 구간 등이 충분히 커버되고 있는지 점검한다.
* 일정 시간이 필요한 테스트
  * 비동기 코드를 테스트할 때는 일정 시간 지연(sleep)이 필요하거나, 이벤트가 트리거되기를 기다리는 방식을 고려해야 한다.
  * 테스트가 지나치게 길어지지 않도록, 가상의 Clock 등을 활용해 효율적으로 시뮬레이션할 수 있다.

**분산 환경 테스트와 통합 커버리지**

ROS2의 핵심은 네트워크를 통한 다중 노드 간의 통신이다. 따라서 테스트 환경이 단일 호스트에서만 수행되는 것이 아니라, 여러 호스트 또는 컨테이너(Docker 등) 간에 분산 실행되어야 하는 경우도 있다.

* 분산 테스트 전략
  * 실제 네트워크 지연이나 QoS 설정 등에 의해 결과가 달라질 수 있으므로, 여러 시나리오(동일 네트워크, 클라우드, 에지 디바이스 등)에서 테스트를 수행하여 커버리지에 반영한다.
* 커버리지 데이터 수합(Collect)
  * 노드별로 생성되는 커버리지 아티팩트가 서로 다른 머신 혹은 컨테이너에 생성될 수 있다.
  * 각 실행 환경에서 생성된 `.gcda`, `.gcno` 등을 중앙 서버나 공유 스토리지로 모아야 최종 통합된 커버리지 보고서를 만들 수 있다.

**실시간(Real-time) 환경에서의 커버리지**

일부 ROS2 시스템(예: 실시간 확장 `rtt_ros2` 등)은 RT-Preempt 커널이나 실시간 프레임워크를 사용한다. 이런 환경에서는 **테스트 자체가 시스템의 타이밍 특성**에 영향을 줄 수 있으므로 주의가 필요하다.

* 인스트루먼테이션 부담
  * 커버리지 측정을 위해 삽입되는 인스트루먼테이션 코드가 실시간성 보장을 저해할 수 있다.
  * 실시간 애플리케이션에서는 개발 단계에서 커버리지를 충분히 측정하되, 실제 실시간 배포 빌드에는 인스트루먼테이션을 제거하는 게 일반적이다.
* 성능 측정 병행
  * 실시간성 코드를 테스트할 때, 함수나 분기에 대한 단순 커버리지 외에 **응답 시간(latency) 측정**이 중요하다.
  * 커버리지와 더불어 성능 프로파일링(예: perf, LTTng 등)을 함께 수행하면, 안정성과 성능 두 측면 모두에서 테스트를 강화할 수 있다.

**지속적으로 업데이트되는 ROS2 패키지**

ROS2는 꾸준히 버전이 업데이트되며, 배포판(Humble, Iron 등) 간에도 마이그레이션되는 API가 많다. 커버리지 측정 자동화 파이프라인을 구축해두면 다음과 같은 장점이 있다.

* 하위 호환성(Backward compatibility) 체크
  * 새 버전의 ROS2로 업데이트 시, 이전 버전에서 작성된 테스트 케이스가 모두 통과하는지 확인 가능.
* API 변경 시 리그레션(Regression) 방지
  * ROS2 핵심 API 사용에 변경이 발생했을 때, 커버리지 하락이나 에러 발생 여부를 신속히 모니터링할 수 있다.

#### 커버리지 측정 시 발생할 수 있는 문제와 해결 방법

커버리지 측정은 일반적인 테스트 실행과 달리 코드에 추가적인 \*\*오버헤드(Overhead)\*\*를 발생시킨다. 다음은 주로 발생하는 문제와 대응 방안이다.

1. **테스트 실행 시간 증가**
   * 인스트루먼테이션된 바이너리는 보통 실행 속도가 느려지고, 파일 입출력이 증가한다.
   * CI 파이프라인에서 테스트 시간을 제한(TTL)하는 경우, 커버리지 측정용 작업을 별도 단계로 분리하거나, 최적화 수준($-Og$ 정도)을 어느 정도 조정하는 방안을 검토한다.
2. **메모리 사용량 증가**
   * 대규모 프로젝트에서 커버리지 데이터를 수집하면, `.gcda` 파일 등의 크기가 상당히 커질 수 있다.
   * 필요 없는 서드파티 라이브러리나 외부 디렉터리는 제외하도록 `lcov --remove` 옵션 등을 활용한다.
3. **동적 라이브러리와 코드 경로 불일치**
   * 커버리지 툴이 소스코드 경로와 실제 실행 바이너리(공유 라이브러리 등)의 경로를 정상적으로 매핑하지 못하면 결과에서 누락이 발생할 수 있다.
   * 빌드 디렉터리 구조, 설치 경로(`install/` vs `build/`)를 명확히 파악하고, 경로 설정을 올바르게 구성해야 한다.

#### 테스트 케이스 보강 전략

커버리지 측정의 궁극적인 목적은 “테스트가 충분한가?”를 확인하고 “부족한 부분에 대한 테스트를 추가”하는 것이다. 커버리지가 낮은 부분(라인, 브랜치, 함수 등)에 대해서는 다음과 같은 프로세스로 보강할 수 있다.

1. 원인 파악
   * 테스트로는 접근하기 어려운 로직인지, 혹은 단순히 테스트 케이스가 누락된 것인지 구분한다.
2. 새로운 테스트 케이스 설계
   * 예외 상황(오류 발생, 예외 처리), 극단적 입력값(엣지 케이스), 멀티스레드 경합 등이 대표적인 누락 원인이 된다.
3. 자동화 및 회귀 테스트 강화
   * 한 번 작성된 테스트 케이스는 CI 파이프라인에 추가해, 코드 수정 후에도 지속적으로 검증되도록 설정한다.
4. 기술 부채(Technical Debt) 청산
   * 불필요하게 복잡한 로직이나 테스트가 어려운 구조는 리팩터링을 고려한다. 간단한 코드로 정리하면 테스트 작성을 수월하게 만들 수 있다.
