개요
ROS2 시스템에서 테스트는 개발 중인 기능이 예상대로 작동하는지 검증하는 중요한 과정이다. 유닛 테스트와 통합 테스트는 각각 다른 수준에서 시스템의 동작을 확인하는 데 사용된다.
- 유닛 테스트는 개별 함수나 모듈 단위의 기능을 검증하는 테스트이다. 이를 통해 코드의 특정 부분이 올바르게 동작하는지 확인한다.
- 통합 테스트는 여러 모듈이 함께 작동할 때 시스템이 예상대로 동작하는지 확인하는 테스트이다.
유닛 테스트의 구조
ROS2에서 유닛 테스트는 주로 Google Test(이하 GTest)를 사용하여 작성된다. GTest는 다양한 테스트 케이스와 어설션을 제공하여 개발자가 개별 기능을 확인할 수 있게 한다.
기본 유닛 테스트 구조
유닛 테스트는 일반적으로 다음과 같은 구조로 작성된다:
#include <gtest/gtest.h>
#include <your_ros2_package/your_module.hpp>
TEST(YourModuleTest, TestFunction) {
// Arrange
YourModule instance;
// Act
int result = instance.your_function();
// Assert
EXPECT_EQ(result, expected_value);
}
int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
이 예시에서 TEST()
매크로는 테스트 케이스를 정의하고, EXPECT_EQ()
는 두 값이 같은지 확인하는 어설션이다.
통합 테스트의 필요성
통합 테스트는 ROS2 시스템의 여러 노드가 상호작용할 때 발생할 수 있는 문제를 미리 발견할 수 있게 한다. 유닛 테스트는 개별 노드나 함수 단위에서 문제를 찾아내지만, 통합 테스트는 시스템 전체의 흐름과 통신을 확인하는 데 중점을 둔다.
통합 테스트는 주로 시스템의 다음 부분들을 검증한다:
- 노드 간의 통신 (퍼블리셔-서브스크라이버)
- 서비스와 액션의 요청-응답 구조
- 시간 동기화 및 데이터 흐름
유닛 테스트 작성법
유닛 테스트는 ROS2의 특정 모듈이나 함수가 예상대로 작동하는지 확인하는 테스트이다. 일반적으로 GTest를 사용하여 다음과 같은 흐름으로 작성된다.
- 테스트 설정: 테스트 대상인 모듈이나 노드를 초기화한다.
- 행동 수행: 테스트 대상에서 특정 동작을 수행하거나 데이터를 처리한다.
- 결과 검증: 결과 값이 예상대로 도출되는지 검증한다.
유닛 테스트 사례: 퍼블리셔 테스트
ROS2에서 노드의 퍼블리셔 기능을 테스트할 때는 다음과 같이 구현할 수 있다:
#include <gtest/gtest.h>
#include <rclcpp/rclcpp.hpp>
#include <std_msgs/msg/string.hpp>
TEST(NodeTest, PublisherTest) {
// Arrange
auto node = rclcpp::Node::make_shared("test_node");
auto publisher = node->create_publisher<std_msgs::msg::String>("topic", 10);
// Act
auto message = std::make_shared<std_msgs::msg::String>();
message->data = "Test Message";
publisher->publish(*message);
// Assert
// 여기에 퍼블리셔 동작을 검증하는 코드 작성
}
int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);
rclcpp::init(argc, argv);
return RUN_ALL_TESTS();
}
위 예제는 노드가 메시지를 퍼블리싱하는지 확인하는 유닛 테스트이다. 주로 퍼블리셔와 서브스크라이버 간의 통신이 제대로 이뤄지는지 확인하는 데 사용된다.
통합 테스트 작성법
통합 테스트는 여러 노드가 상호작용하는 상황에서 시스템의 전체적인 동작을 검증한다. 이를 위해 ROS2는 launch_testing
패키지를 제공한다. 이 패키지를 사용하면 복잡한 시스템 구성에서도 테스트를 수행할 수 있다.
import launch
import launch_ros.actions
import launch_testing
import unittest
def generate_test_description():
talker_node = launch_ros.actions.Node(
package='demo_nodes_cpp', executable='talker', output='screen'
)
listener_node = launch_ros.actions.Node(
package='demo_nodes_cpp', executable='listener', output='screen'
)
return launch.LaunchDescription([
talker_node,
listener_node,
launch_testing.actions.ReadyToTest(),
]), {
'talker': talker_node,
'listener': listener_node,
}
class TestTalkerListener(unittest.TestCase):
def test_nodes_exist(self, talker, listener):
self.assertIsNotNone(talker)
self.assertIsNotNone(listener)
def test_communication(self):
# 노드 간 통신을 확인하는 테스트 로직 추가
pass
이 예제는 ROS2의 기본적인 통합 테스트 사례이다. talker
와 listener
노드가 제대로 실행되고 있는지 확인하고, 그들 간의 통신을 검증하는 테스트이다.
Module obj;
// Act
auto result = obj.yourFunction();
// Assert
EXPECT_EQ(result, expected_value);
}
이 구조는 Arrange-Act-Assert
패턴을 따른다. 즉, 테스트에 필요한 준비를 한 후(Arrange
), 테스트 대상인 함수를 실행(Act
)하고, 그 결과가 예상한 값과 일치하는지(Assert
) 확인한다.
GTest 주요 어설션
EXPECT_EQ(a, b)
:a
와b
가 같은지 확인한다.EXPECT_NE(a, b)
:a
와b
가 다른지 확인한다.EXPECT_TRUE(condition)
: 조건이 참인지 확인한다.EXPECT_FALSE(condition)
: 조건이 거짓인지 확인한다.
예제: 유닛 테스트
아래는 ROS2 패키지에서 퍼블리셔 노드의 기본 기능을 테스트하는 예시이다.
#include <gtest/gtest.h>
#include <rclcpp/rclcpp.hpp>
#include <std_msgs/msg/string.hpp>
class TestPublisher : public ::testing::Test {
protected:
void SetUp() override {
rclcpp::init(0, nullptr);
node_ = std::make_shared<rclcpp::Node>("test_publisher_node");
}
void TearDown() override {
rclcpp::shutdown();
}
rclcpp::Node::SharedPtr node_;
};
TEST_F(TestPublisher, PublishMessage) {
auto publisher = node_->create_publisher<std_msgs::msg::String>("test_topic", 10);
auto message = std_msgs::msg::String();
message.data = "Hello, ROS2!";
EXPECT_NO_THROW(publisher->publish(message));
}
이 테스트는 TestPublisher
클래스를 사용하여 퍼블리셔 노드에서 메시지를 퍼블리싱할 때 오류가 발생하지 않는지 확인하는 간단한 유닛 테스트이다.
유닛 테스트와 ROS2 런타임 통합
유닛 테스트는 ROS2 런타임과 통합될 수 있다. 예를 들어, 노드를 실행하는 동안 기능을 테스트하고, 이를 통해 각종 콜백 함수나 통신 구조를 검증할 수 있다.
노드의 콜백 함수 테스트
콜백 함수는 ROS2 시스템에서 매우 중요하다. 다음은 콜백 함수가 올바르게 작동하는지 확인하는 유닛 테스트 예제이다:
#include <gtest/gtest.h>
#include <rclcpp/rclcpp.hpp>
#include <std_msgs/msg/string.hpp>
class TestSubscriber : public ::testing::Test {
protected:
void SetUp() override {
rclcpp::init(0, nullptr);
node_ = std::make_shared<rclcpp::Node>("test_subscriber_node");
message_received_ = false;
}
void TearDown() override {
rclcpp::shutdown();
}
void callback(const std_msgs::msg::String::SharedPtr msg) {
message_received_ = true;
EXPECT_EQ(msg->data, "Hello, ROS2!");
}
rclcpp::Node::SharedPtr node_;
bool message_received_;
};
TEST_F(TestSubscriber, ReceiveMessage) {
auto subscriber = node_->create_subscription<std_msgs::msg::String>(
"test_topic", 10, std::bind(&TestSubscriber::callback, this, std::placeholders::_1));
auto publisher = node_->create_publisher<std_msgs::msg::String>("test_topic", 10);
auto message = std_msgs::msg::String();
message.data = "Hello, ROS2!";
publisher->publish(message);
// Spin to process callbacks
rclcpp::spin_some(node_);
EXPECT_TRUE(message_received_);
}
이 예제에서는 퍼블리셔가 메시지를 퍼블리싱하고, 서브스크라이버의 콜백 함수가 메시지를 수신하는지 확인한다.
통합 테스트
통합 테스트는 서로 다른 노드가 함께 작동할 때의 상호 작용을 확인하는데 초점을 맞춘다. ROS2에서 통합 테스트는 launch_testing
모듈을 사용하여 노드가 실제로 실행되는 동안 그 동작을 검증할 수 있다.
통합 테스트의 기본 구조
통합 테스트는 런치 파일을 통해 노드를 실행하고, 테스트 케이스를 사용하여 해당 노드들의 상호 작용을 검증한다.
예제: 두 개의 노드가 통신하는 통합 테스트
import os
import launch
import launch_ros.actions
import pytest
import launch_testing
from launch import LaunchDescription
@pytest.mark.launch_test
def generate_test_description():
publisher_node = launch_ros.actions.Node(
package='your_package',
executable='publisher_node',
name='publisher_node'
)
subscriber_node = launch_ros.actions.Node(
package='your_package',
executable='subscriber_node',
name='subscriber_node'
)
return LaunchDescription([
publisher_node,
subscriber_node,
launch_testing.actions.ReadyToTest()
])
def test_nodes_communication():
# 테스트 코드
pass
이 코드에서 generate_test_description()
함수는 두 개의 노드를 런치 파일을 통해 실행한다. test_nodes_communication()
함수는 두 노드 간의 통신이 제대로 이루어졌는지 확인하는 테스트를 구현하는 자리이다. 통합 테스트는 실제 실행 환경에서 노드 간 상호 작용을 검증하는 데 중점을 둔다.
통합 테스트 실행과 검증
통합 테스트는 launch_testing
패키지를 통해 ROS2 노드들을 실행하고, 그 상호작용을 확인할 수 있는 매우 강력한 도구이다. 다음은 통합 테스트의 실제 예제이다.
예제: 퍼블리셔와 서브스크라이버의 통합 테스트
import os
import launch
import launch_ros.actions
import pytest
import launch_testing
from launch import LaunchDescription
from launch_ros.actions import Node
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
@pytest.mark.launch_test
def generate_test_description():
publisher_node = Node(
package='your_package',
executable='publisher_node',
name='publisher_node'
)
subscriber_node = Node(
package='your_package',
executable='subscriber_node',
name='subscriber_node'
)
return LaunchDescription([
publisher_node,
subscriber_node,
launch_testing.actions.ReadyToTest()
])
def test_nodes_communication():
rclpy.init()
try:
# 노드 간의 상호작용 테스트를 여기서 구현한다.
pass
finally:
rclpy.shutdown()
이 테스트는 두 개의 노드가 통신하는지 검증하는 구조를 보여준다. generate_test_description()
함수는 퍼블리셔와 서브스크라이버 노드를 실행시키고, test_nodes_communication()
함수는 그들 사이의 상호작용을 테스트한다.
타임아웃 설정
ROS2 노드들은 실시간으로 동작하기 때문에 타임아웃 설정이 필요할 수 있다. launch_testing
에서는 타임아웃을 설정하여, 특정 시간 내에 원하는 조건이 충족되지 않으면 테스트가 실패하도록 할 수 있다.
@pytest.mark.launch_test(timeout=10)
def test_nodes_communication():
rclpy.init()
try:
# 10초 내에 테스트가 끝나지 않으면 타임아웃 발생
pass
finally:
rclpy.shutdown()
타임아웃을 설정함으로써 노드 간의 통신이 적절한 시간 내에 이루어지는지 확인할 수 있다.
ROS2 런치 테스트 설정
통합 테스트에서 여러 노드를 동시에 실행하기 위해 런치 파일을 사용한다. launch_testing
을 사용하면 실행 중인 노드들 간의 상호작용을 테스트할 수 있다.
import launch
import launch_ros.actions
import launch_testing
import pytest
@pytest.mark.launch_test
def generate_test_description():
return launch.LaunchDescription([
launch_ros.actions.Node(
package='your_package',
executable='node_a',
name='node_a'
),
launch_ros.actions.Node(
package='your_package',
executable='node_b',
name='node_b'
),
launch_testing.actions.ReadyToTest()
])
def test_node_communication():
# 여기서 두 노드의 상호작용을 테스트
pass
위 코드는 노드 A와 B가 상호작용하는 환경에서의 통합 테스트 설정 예시이다.
메시지 검증
노드들 간의 통신에서 주고받는 메시지를 확인하는 것이 중요하다. 이를 위해, launch_testing
은 테스트 실행 중에 각 노드에서 주고받는 메시지를 모니터링할 수 있다.
import launch_testing
from launch_ros.actions import Node
import rclpy
def test_nodes_communication():
rclpy.init()
# 메시지를 발행하는 퍼블리셔 노드
publisher = node.create_publisher(String, 'topic', 10)
# 메시지를 구독하는 서브스크라이버 노드
received_messages = []
def callback(msg):
received_messages.append(msg)
subscriber = node.create_subscription(
String,
'topic',
callback,
10
)
# 퍼블리셔에서 메시지 발행
msg = String()
msg.data = 'Hello, ROS2'
publisher.publish(msg)
# 스핀을 통해 콜백이 호출될 수 있도록 함
rclpy.spin_once(node)
# 메시지가 제대로 수신되었는지 확인
assert len(received_messages) > 0
assert received_messages[0].data == 'Hello, ROS2'
rclpy.shutdown()
이 예시에서는 퍼블리셔가 발행한 메시지를 서브스크라이버가 제대로 수신했는지 확인한다. 메시지가 callback()
함수에서 처리되고, 그 값이 예상과 일치하는지 검증하는 부분이 중요하다.
통합 테스트의 시나리오 구성
통합 테스트는 다양한 시나리오를 구성하여 여러 노드의 상호작용을 종합적으로 테스트할 수 있다. 예를 들어, 다음과 같은 시나리오를 구성할 수 있다:
- 노드 간 기본 통신 확인: 퍼블리셔와 서브스크라이버가 예상대로 데이터를 주고받는지 확인한다.
- 비정상적인 상황 테스트: 메시지 손실, 지연, 노드 충돌 등 비정상적인 상황에서 시스템이 어떻게 반응하는지 확인한다.
- 네트워크 구성에 따른 성능 검증: 네트워크 설정에 따라 메시지 전달 속도와 성능이 어떻게 달라지는지 테스트한다.
통합 테스트는 시스템 전반의 상호작용을 검증하는 중요한 도구로, 이를 통해 개발 과정에서의 오류를 조기에 발견하고, 안정적인 시스템을 구축할 수 있다.