개요

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를 사용하여 다음과 같은 흐름으로 작성된다.

  1. 테스트 설정: 테스트 대상인 모듈이나 노드를 초기화한다.
  2. 행동 수행: 테스트 대상에서 특정 동작을 수행하거나 데이터를 처리한다.
  3. 결과 검증: 결과 값이 예상대로 도출되는지 검증한다.

유닛 테스트 사례: 퍼블리셔 테스트

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의 기본적인 통합 테스트 사례이다. talkerlistener 노드가 제대로 실행되고 있는지 확인하고, 그들 간의 통신을 검증하는 테스트이다.

    Module obj;

    // Act
    auto result = obj.yourFunction();

    // Assert
    EXPECT_EQ(result, expected_value);
}

이 구조는 Arrange-Act-Assert 패턴을 따른다. 즉, 테스트에 필요한 준비를 한 후(Arrange), 테스트 대상인 함수를 실행(Act)하고, 그 결과가 예상한 값과 일치하는지(Assert) 확인한다.

GTest 주요 어설션

예제: 유닛 테스트

아래는 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() 함수에서 처리되고, 그 값이 예상과 일치하는지 검증하는 부분이 중요하다.

통합 테스트의 시나리오 구성

통합 테스트는 다양한 시나리오를 구성하여 여러 노드의 상호작용을 종합적으로 테스트할 수 있다. 예를 들어, 다음과 같은 시나리오를 구성할 수 있다:

  1. 노드 간 기본 통신 확인: 퍼블리셔와 서브스크라이버가 예상대로 데이터를 주고받는지 확인한다.
  2. 비정상적인 상황 테스트: 메시지 손실, 지연, 노드 충돌 등 비정상적인 상황에서 시스템이 어떻게 반응하는지 확인한다.
  3. 네트워크 구성에 따른 성능 검증: 네트워크 설정에 따라 메시지 전달 속도와 성능이 어떻게 달라지는지 테스트한다.

통합 테스트는 시스템 전반의 상호작용을 검증하는 중요한 도구로, 이를 통해 개발 과정에서의 오류를 조기에 발견하고, 안정적인 시스템을 구축할 수 있다.