# ROS2 전용 테스트 프레임워크 개요

#### ROS2 테스트 개념과 필요성

ROS2에서의 테스트는 시스템의 안정성과 신뢰성을 보장하기 위해 필수적으로 수행해야 하는 단계이다. 특히 여러 노드가 상호작용하며 동작하는 분산 구조에서, 각 노드의 독립적인 동작뿐 아니라 상호 간의 메시지 교환이 의도한 대로 이뤄지는지 검사하는 과정은 매우 중요하다. 이를 위해 ROS2는 자체적으로 테스트 프레임워크와 도구들을 제공하며, 다음과 같은 특징을 가진다.

* 단위(Unit) 테스트부터 통합(Integration) 테스트, 시스템(System) 테스트까지 폭넓게 지원한다.
* ament 빌드 시스템과 긴밀히 연동되어 빌드 시 자동으로 테스트가 실행되도록 설정할 수 있다.
* C++, Python, 그리고 ROS2의 특정 기능(토픽, 서비스, 액션 등)을 간편하게 테스트하도록 전용 테스트 라이브러리를 제공한다.

#### ament와 테스트

ROS2 패키지의 빌드 시스템으로 ament\_cmake 또는 ament\_python 등을 사용하는데, 이때 테스트 코드는 자동으로 테스트 타겟에 포함된다. CMakeLists.txt 또는 setup.py에 미리 테스트 관련 설정을 포함해두면, 사용자는 추가적인 명령어 없이도 아래 명령으로 프로젝트 빌드와 동시에 테스트를 수행할 수 있다.

```bash
colcon build --symlink-install --cmake-args -DBUILD_TESTING=ON
colcon test
colcon test-result
```

여기서 `colcon test`는 `BUILD_TESTING=ON`인 경우 테스트가 존재하는 모든 패키지의 테스트를 자동으로 수행한다. 이후 `colcon test-result`를 통해 종합 결과를 확인할 수 있다.

#### ament\_cmake 테스트 구조

C++ 기반 ROS2 패키지에서 주로 사용하는 `ament_cmake` 빌드 방식을 예로 들면, 테스트를 위해 다음과 같은 구성을 갖는다.

* **테스트 실행 파일**: GTest, GMock 등을 포함해 작성된 C++ 테스트 소스.
* **테스트 선언**: CMakeLists.txt에 `ament_add_gtest()` 매크로로 테스트 실행 파일을 등록.
* **빌드 옵션**: `BUILD_TESTING` CMake 옵션을 통해 테스트 빌드 활성화 여부를 결정.

아래는 간단한 예시이다.

```cmake
if(BUILD_TESTING)
  find_package(ament_cmake_gtest REQUIRED)
  ament_add_gtest(my_test test/test_example.cpp)
  if(TARGET my_test)
    target_include_directories(my_test PRIVATE include)
  endif()
endif()
```

위 예시에서 `ament_cmake_gtest`를 이용해 GTest가 적용된 테스트 타겟을 만들고, `my_test`라는 이름으로 등록한다. 이후 `my_test` 타겟에 필요한 헤더 경로 등 추가 설정을 수행한다.

#### ament\_python 테스트 구조

Python 기반 ROS2 패키지에서는 `ament_python` 빌드 방식을 사용한다. 이 경우 `setup.py`에 `entry_points`나 `pytest` 관련 설정을 추가해 테스트를 등록한다. 예를 들면 다음과 같은 구성이 가능하다.

```python
from setuptools import setup

package_name = 'my_python_pkg'

setup(
    name=package_name,
    ...
    tests_require=['pytest'],
    entry_points={
        'console_scripts': [
            'my_node = my_python_pkg.my_node:main',
        ],
    },
)
```

그리고 패키지 디렉터리에 `tests` 폴더를 두고, 그 내부에 `test_something.py` 파일을 둔다면, colcon 빌드 과정에서 자동으로 `pytest` 기반 테스트가 수행된다.

#### launch\_testing과 통합 테스트

ROS2에서 노드와 노드 사이의 상호작용을 실제 구동 환경과 유사하게 테스트하고 싶다면, 단순히 GTest나 PyTest만으로는 부족하다. 이럴 때 사용하는 것이 ROS2에서 제공하는 통합 테스트 도구인 **launch\_testing**이다. 이 도구는 다음과 같은 특징을 갖는다.

* **ROS2 launch 파일**과 함께 동작하여, 테스트 대상 노드들을 실제 실행 프로세스로 띄운 뒤 테스트 스크립트가 이를 관찰하고 검증한다.
* **Python 기반**으로 작성되어, 테스트 흐름을 다양한 방식으로 구성 가능하다.
* **ROS2 통신 기능**(토픽, 서비스, 액션 등)의 실질적인 동작을 확인할 수 있다. 예컨대, 특정 메시지가 실제로 퍼블리시되는지, 서비스 요청에 올바르게 응답하는지 등을 테스트 코드에서 직접 확인한다.

launch\_testing을 이용하는 과정은 크게 다음 단계로 요약할 수 있다.

1. **launch 파일 준비**: 테스트를 위해 실행할 노드들을 명시한 ROS2 launch 파일이 필요하다.
2. **테스트 스크립트 작성**: Python의 unittest 혹은 pytest 스타일로 작성하며, 실행된 노드들의 동작 상태를 점검한다.
3. **종합 실행**: launch\_testing이 제공하는 특정 인터페이스를 통해, 테스트 스크립트와 launch 파일을 결합해 테스트를 수행한다.

예를 들어, 다음과 같은 구조를 가정해보자.

```
my_pkg/
└── test/
    ├── test_integration.launch.py
    └── test_integration.py
```

**예시: test\_integration.launch.py**

```python
import launch
import launch_ros.actions

def generate_launch_description():
    talker_node = launch_ros.actions.Node(
        package='my_pkg',
        executable='talker',
        name='talker'
    )
    listener_node = launch_ros.actions.Node(
        package='my_pkg',
        executable='listener',
        name='listener'
    )

    return launch.LaunchDescription([
        talker_node,
        listener_node
    ])
```

* 여기서는 ROS2 노드인 `talker`와 `listener`를 동시에 실행하도록 구성했다.

**예시: test\_integration.py**

```python
import launch_testing
import unittest
import rclpy
from rclpy.node import Node

def generate_test_description():
    # launch 파일을 불러오는 대신 직접 Node를 생성하거나
    # launch_testing.actions.ReadyToTest() 등을 사용할 수도 있음
    return launch_testing.util.get_default_test_description()

class TestTalkerListenerIntegration(unittest.TestCase):

    @classmethod
    def setUpClass(cls):
        rclpy.init()

    @classmethod
    def tearDownClass(cls):
        rclpy.shutdown()

    def test_example(self):
        # 실제 메시지 송수신 테스트 로직
        self.assertTrue(True)

# 실제 테스트 실행 구문
# launch_testing 기능을 이용해 상기 테스트를 수행
```

위 코드는 가장 단순화된 형태로, 실제로는 `generate_test_description()`에서 launch 파일을 함께 불러오는 방식이 권장된다. 실제 예시를 좀 더 살펴보면:

```python
import pytest
import rclpy
from rclpy.node import Node
import launch
import launch_ros.actions
import launch_testing
import unittest

@pytest.mark.rostest
def generate_test_description():
    talker_node = launch_ros.actions.Node(
        package='my_pkg',
        executable='talker',
        name='talker'
    )
    listener_node = launch_ros.actions.Node(
        package='my_pkg',
        executable='listener',
        name='listener'
    )

    # 테스트에 필요한 Node 실행 후, ReadyToTest 액션을 호출
    return launch.LaunchDescription([
        talker_node,
        listener_node,
        launch_testing.actions.ReadyToTest()
    ]), {
        'talker': talker_node,
        'listener': listener_node
    }

class TestTalkerListenerCommunication(unittest.TestCase):

    @classmethod
    def setUpClass(cls):
        rclpy.init()

    @classmethod
    def tearDownClass(cls):
        rclpy.shutdown()

    def test_talker_publication(self):
        # talker가 실제로 메시지를 퍼블리시하는지 검증하기
        self.assertTrue(True)

    def test_listener_subscription(self):
        # listener가 해당 메시지를 받아서 처리하는지 검증하기
        self.assertTrue(True)
```

이런 식으로 **노드를 실제로 실행**한 뒤 **테스트 함수**에서 메시지 송수신을 테스트할 수 있다. 또한 launch\_testing을 실행하려면 다음과 같이 `pytest`를 활용해 수행한다.

```bash
colcon test --packages-select my_pkg
colcon test-result
```

pytest 기반에서는 `@pytest.mark.rostest` 데커레이터를 사용하여, ROS2의 launch\_testing과 결합한 테스트임을 명시한다.

#### launch\_testing\_ros

launch\_testing\_ros는 launch\_testing에 ROS2 전용 유틸리티가 추가된 확장판으로, 토픽, 서비스, 파라미터 등을 보다 편리하게 검사할 수 있는 기능을 제공한다. 예컨대, 특정 노드가 퍼블리시하는 토픽의 메시지를 일정 시간 안에 반드시 수신해야 한다거나, 특정 서비스가 호출될 때의 응답을 확인하는 작업 등을 쉽게 설정할 수 있다.

#### 코드 커버리지와 ament\_cmake의 연동

테스트의 완성도를 높이기 위해서는 단순한 테스트 실행 결과(통과/실패)뿐 아니라, 코드의 어느 부분까지 테스트가 수행되었는지 정량적으로 확인할 필요가 있다. 이를 위해 ROS2의 빌드 시스템(ament\_cmake)을 활용하여 코드 커버리지를 측정할 수 있다. 일반적으로 다음과 같은 툴체인이 사용된다.

* **gcov**: GCC 컴파일러에서 제공하는 커버리지 측정 도구
* **lcov**: gcov의 결과를 수집·정리하여 HTML 리포트 등으로 시각화
* **CMake 설정**: `BUILD_TESTING=ON`과 함께 컴파일 플래그(--coverage 등)를 추가 설정

아래는 간단한 CMakeLists.txt 예시이다.

```cmake
if(BUILD_TESTING)
  # GTest 관련 설정
  find_package(ament_cmake_gtest REQUIRED)
  ament_add_gtest(my_test test/test_example.cpp)

  # 커버리지 측정용 플래그 설정
  if(CMAKE_COMPILER_IS_GNUCXX)
    message(STATUS "Enabling coverage flags")
    set(COVERAGE_FLAGS "--coverage")
    add_compile_options(${COVERAGE_FLAGS})
    add_link_options(${COVERAGE_FLAGS})
  endif()

  # 가능하다면 ament_cmake_gtest 대신 ament_add_gmock을 사용하는 등 확장 가능
endif()
```

이렇게 설정해둔 뒤 테스트를 수행하면, `gcov` 파일이 생성되고 이를 `lcov`로 후처리할 수 있다.

```bash
# 빌드 및 테스트
colcon build --cmake-args -DBUILD_TESTING=ON
colcon test

# gcov 결과를 lcov로 수집
lcov --capture --directory . --output-file coverage.info

# 불필요한(시스템 헤더 등) 영역을 제외
lcov --remove coverage.info '/usr/*' --output-file coverage.info

# HTML 리포트 생성
genhtml coverage.info --output-directory coverage_report
```

결과적으로 `coverage_report` 디렉터리에 HTML 형식의 리포트가 생성되며, 이를 통해 테스트가 실제로 어느 소스 코드를 얼마나 커버했는지 확인할 수 있다.

#### ament\_lint 자동화

ROS2에서는 코드 스타일, 포맷, 정적 분석 등을 자동으로 검사할 수 있는 **ament\_lint** 계열의 패키지들을 제공한다. 예컨대 다음과 같은 린트(lint) 패키지가 있다.

* **ament\_lint\_auto**: 여러 린트 플러그인을 한 번에 적용
* **ament\_cpplint**, **ament\_uncrustify**: C++ 코드 스타일 검사
* **ament\_pep257**, **ament\_pep8**: Python 코드 스타일 검사
* **ament\_xmllint**: XML 포맷 검사

C++ 패키지의 CMakeLists.txt에서 다음과 같이 선언함으로써 자동화가 가능하다.

```cmake
find_package(ament_lint_auto REQUIRED)
ament_lint_auto_find_test_dependencies()
```

그러면 colcon 빌드 시에 자동으로 스타일 및 포맷 검사가 이루어지며, 문제 발견 시 테스트가 실패한다. Python 패키지에서도 setup.py 혹은 해당되는 파일에 유사하게 설정할 수 있다. 이를 통해 단위 테스트, 통합 테스트 뿐 아니라 **코드 품질** 측면에서도 일관된 관리를 할 수 있다.

#### 특수한 테스트 요구사항

ROS2 기반 로봇 애플리케이션에서는 일반적인 유닛 테스트와 통합 테스트 외에도 아래와 같은 요구사항이 자주 등장한다.

1. **실시간 성능 시험**: 특정 노드가 주기적으로 퍼블리시하는 토픽의 주기가 매우 짧은 경우, 실제로 지연이 발생하지 않는지 측정.
2. **하드웨어 의존성 시험**: 센서나 액추에이터가 연결된 물리 장비와 상호작용하는 노드의 테스트. 이 경우 HIL(Hardware In the Loop) 테스트나 시뮬레이터를 활용하기도 한다.
3. **장시간 스트레스 시험**: 로봇 애플리케이션이 오랜 시간 동안 안정적으로 동작하는지 살펴보는 내구성 테스트.

이러한 특수 요구사항에서는 ROS2의 launch\_testing, rosbag 등을 조합해서 실제 데이터를 재현하거나 시뮬레이터 환경을 구성해 테스트할 수 있다.

#### ros2test 유틸리티

ROS2 Foxy 이후 버전부터는 편의성을 높이기 위해 **ros2test** 패키지가 제공되기 시작했다. 이는 ROS2에서 테스트를 좀 더 일관성 있게 수행할 수 있도록 돕는 도구로, 크게 다음과 같은 기능을 갖는다.

* **테스트 자동 디스커버리**: ros2test 명령어가 패키지 내의 테스트를 자동으로 찾고 실행
* **별도의 프로세스 관리**: 여러 노드를 띄우고 종료하는 과정을 편리하게 제어
* **로그/출력 관리**: 각 노드 및 테스트의 로그를 체계적으로 수집

아직 다른 툴보다 사용 사례가 많진 않지만, 버전이 올라가면서 점차 안정화되고 있는 추세다.

#### 고급 테스트 기법과 모범 사례

**Mocking과 의존성 분리**

ROS2 노드는 보통 외부 시스템(예: 센서, 액추에이터, 클라우드 서비스 등)에 의존성을 가지는 경우가 많다. 이때 외부 시스템이 실제로 연결되어 있지 않더라도 테스트가 가능하도록, **Mock** 객체나 \*\*테스트 더블(Test Double)\*\*을 활용하여 의존성을 분리하는 기법이 중요하다. C++에서는 Google Mock(GMock) 라이브러리를 사용하는 사례가 많고, Python에서는 unittest.mock을 주로 사용한다.

예를 들어, 특정 센서 메시지를 구독하는 노드가 있다고 할 때, 실제 센서 없이도 테스트가 가능하도록 가짜 퍼블리셔(Mock Publisher)를 생성해 해당 노드에 메시지를 보내고, 노드의 응답 혹은 내부 상태 변화를 확인한다. 이를 통해 하드웨어 환경이 갖춰지지 않은 CI(Continuous Integration) 환경에서도 테스트를 자동화할 수 있다.

**파라미터(Parameters) 테스트**

ROS2 노드는 토픽, 서비스 뿐 아니라 \*\*파라미터(Parameters)\*\*를 통해 런타임에 동작 방식을 바꿀 수 있다. 예컨대 로봇 주행 속도, 센서 업데이트 주기 등을 파라미터로 정의해 놓을 때, 이를 다양한 값으로 바꿔가며 노드 동작을 검증하는 것은 매우 중요한 작업이다. 이때 다음과 같은 접근을 할 수 있다.

* **PyTest parametrize**: Python 테스트에서 `@pytest.mark.parametrize` 데커레이터를 이용해 파라미터 값들을 간단히 반복 테스트한다.
* **GTest value-parameterized tests**: C++ 테스트에서 `INSTANTIATE_TEST_SUITE_P` 매크로 등을 이용해 여러 파라미터 조합으로 테스트를 반복 수행한다.
* **자동화 스크립트**: launch\_testing을 통해 특정 파라미터 세트를 넣어 노드를 띄우고, 결과를 관찰하는 과정을 스크립트화한다.

이를 통해 단일 코드베이스로 다양한 환경 설정을 시험해볼 수 있고, 버그가 특정 파라미터 조합에서만 발생하는 경우를 조기에 발견하기에도 유용하다.

**멀티 노드/멀티 로봇 시나리오**

ROS2는 분산 환경을 염두에 두고 설계되었으므로, 복수의 노드가 동시에 통신하는 시나리오가 흔하다. 더 나아가 멀티 로봇 환경에서 각 로봇이 서로 다른 네임스페이스(namespace)로 노드를 구동하거나, 토픽을 구독/퍼블리시하는 복수의 로봇 네트워크를 시뮬레이션하는 등의 복잡한 테스트가 필요할 수 있다. 이를 수행하기 위한 일반적인 방법은 아래와 같다.

1. **멀티플 launch 파일**: launch\_testing에서 여러 개의 노드를 서로 다른 네임스페이스에 할당하여 동시에 띄운 뒤, 서로의 상태를 모니터링한다.
2. **Mock 로봇 노드**: 실제 로봇 대신에 각종 토픽을 유발하는 노드를 가짜로 띄워, 통신 흐름만 검증한다.
3. **네트워크 설정 테스트**: ROS2의 DDS(Datadistribution Service) 설정을 바꿔가며, QoS(품질 정책) 테스트 등을 병행한다.

**실시간성 검증**

실시간성이 중요한 로봇 제어나 산업용 시스템에서는 메시지 송수신 지연, 주기 지연 등이 치명적인 이슈가 될 수 있다. 따라서 단순히 메시지가 올바른 내용을 전달하는지뿐 아니라, **얼마나 빠르고 안정적으로** 전달되는지도 측정해야 한다.

* **Timestamp 기반 측정**: talker와 listener 노드에서 송신 시각, 수신 시각을 각각 기록하고, 두 시각의 차이로 지연 시간(latency)을 구한다.
* **Cycle time 측정**: 주기적으로 토픽을 퍼블리시하는 노드가 실제로 설정한 주기를 만족하는지(예: 100 Hz, 1 kHz 등) 연속 샘플링하여 통계 분석.
* **부하(Load) 테스트**: CPU, 네트워크 사용량이 증가했을 때, 여전히 실시간성을 지킬 수 있는지 확인한다.

**시뮬레이션 도구 연동**

Gazebo, Ignition, Webots 등의 시뮬레이션 환경과 연동하여 테스트를 자동화하면, 물리적인 로봇 없이도 복잡한 동작 시나리오를 시험할 수 있다. 이 경우 **ROS2 launch 파일**에서 시뮬레이터를 자동으로 구동한 뒤, 시뮬레이터 내에서 가상의 센서, 액추에이터를 통해 노드를 테스트할 수 있다. 이를테면 다음과 같은 구조가 가능하다.

{% @mermaid/diagram content="flowchart LR
A\[Launch \n Test] --> B\["Run \n Simulator \n (Gazebo)"]
B --> C\[Spawn Robot Model \n in \n Simulation]
C --> D\[Test Node \n with \n Simulated \n Topics/Services]
D --> E\[Assertions \n on Node \n Outputs]" %}

**CI/CD 환경과 통합**

GitHub Actions, GitLab CI, Jenkins 등 CI/CD 도구에서 ROS2 테스트를 통합 실행하도록 구성하면, Pull Request(또는 Merge Request) 시 자동 빌드 및 테스트가 수행되어 **코드 품질**과 **기능 유효성**을 수시로 점검할 수 있다. 예시 GitHub Actions 워크플로우 YAML은 아래와 비슷한 형태가 될 수 있다.

```yaml
name: ROS2 CI
on: [push, pull_request]

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Setup ROS2
        uses: ros-tooling/setup-ros@v0.2
        with:
          required-ros-distributions: humble
      - name: Install dependencies
        run: |
          rosdep install --from-paths src --ignore-src -y
      - name: Build
        run: colcon build --cmake-args -DBUILD_TESTING=ON
      - name: Test
        run: |
          colcon test
          colcon test-result --verbose
```

이렇게 자동화하면 팀원 모두가 코드를 변경할 때마다 실시간으로 테스트 결과를 확인할 수 있으므로, 신속한 피드백과 안정적인 품질 관리가 가능해진다.

#### 잠재적 문제와 디버깅 팁

테스트 환경을 구성하다 보면 다음과 같은 문제를 자주 겪는다.

* **DDS 설정 불일치**: 로컬 네트워크 설정(Domain ID, QoS, Discovery 설정 등)이 달라서 노드가 서로 인식되지 않는 경우
* **타이밍 이슈**: 노드가 초기화되는 시간을 충분히 주지 않은 채 메시지를 보냈을 때, 간헐적으로 테스트가 실패하는 경우
* **ROS\_DOMAIN\_ID 충돌**: 동시에 여러 테스트를 실행하면서 같은 Domain ID를 사용해 노드들이 뒤섞이는 경우

이러한 문제를 해결하기 위해서는 아래와 같은 팁을 활용할 수 있다.

* **launch\_testing의 post\_shutdown\_test**: 노드가 종료된 뒤에도 일부 확인 로직을 수행하는 기능을 사용해, 종료 시점 문제를 디버깅할 수 있다.
* **로깅 수준 조정**: rclcpp의 RCLCPP\_DEBUG, RCLCPP\_INFO 등을 적절히 조정하여 노드 내부 상태를 상세히 기록한다.
* **계층적 테스트**: 유닛 테스트로 먼저 내부 로직을 검증한 뒤, 통합 테스트로 메시지 흐름을 검증함으로써 문제 지점을 좁히기 용이해진다.
