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

ROS2 테스트 개념과 필요성

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

  • 단위(Unit) 테스트부터 통합(Integration) 테스트, 시스템(System) 테스트까지 폭넓게 지원한다.

  • ament 빌드 시스템과 긴밀히 연동되어 빌드 시 자동으로 테스트가 실행되도록 설정할 수 있다.

  • C++, Python, 그리고 ROS2의 특정 기능(토픽, 서비스, 액션 등)을 간편하게 테스트하도록 전용 테스트 라이브러리를 제공한다.

ament와 테스트

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

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

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

ament_cmake 테스트 구조

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

  • 테스트 실행 파일: GTest, GMock 등을 포함해 작성된 C++ 테스트 소스.

  • 테스트 선언: CMakeLists.txt에 ament_add_gtest() 매크로로 테스트 실행 파일을 등록.

  • 빌드 옵션: BUILD_TESTING CMake 옵션을 통해 테스트 빌드 활성화 여부를 결정.

아래는 간단한 예시이다.

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

ament_python 테스트 구조

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

그리고 패키지 디렉터리에 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 파일을 결합해 테스트를 수행한다.

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

예시: test_integration.launch.py

  • 여기서는 ROS2 노드인 talkerlistener를 동시에 실행하도록 구성했다.

예시: test_integration.py

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

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

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 예시이다.

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

결과적으로 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에서 다음과 같이 선언함으로써 자동화가 가능하다.

그러면 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 파일에서 시뮬레이터를 자동으로 구동한 뒤, 시뮬레이터 내에서 가상의 센서, 액추에이터를 통해 노드를 테스트할 수 있다. 이를테면 다음과 같은 구조가 가능하다.

spinner

CI/CD 환경과 통합

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

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

잠재적 문제와 디버깅 팁

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

  • DDS 설정 불일치: 로컬 네트워크 설정(Domain ID, QoS, Discovery 설정 등)이 달라서 노드가 서로 인식되지 않는 경우

  • 타이밍 이슈: 노드가 초기화되는 시간을 충분히 주지 않은 채 메시지를 보냈을 때, 간헐적으로 테스트가 실패하는 경우

  • ROS_DOMAIN_ID 충돌: 동시에 여러 테스트를 실행하면서 같은 Domain ID를 사용해 노드들이 뒤섞이는 경우

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

  • launch_testing의 post_shutdown_test: 노드가 종료된 뒤에도 일부 확인 로직을 수행하는 기능을 사용해, 종료 시점 문제를 디버깅할 수 있다.

  • 로깅 수준 조정: rclcpp의 RCLCPP_DEBUG, RCLCPP_INFO 등을 적절히 조정하여 노드 내부 상태를 상세히 기록한다.

  • 계층적 테스트: 유닛 테스트로 먼저 내부 로직을 검증한 뒤, 통합 테스트로 메시지 흐름을 검증함으로써 문제 지점을 좁히기 용이해진다.

Last updated