# GTest 기반 C++ 단위 테스트 작성

#### GTest란?

Google Test(이하 GTest)는 Google에서 개발한 C++ 단위 테스트 프레임워크로, 간결하고 직관적인 API를 통해 다양한 테스트 케이스를 작성할 수 있다. ROS 2 Humble 환경에서도 GTest를 활용하여 각종 기능별 단위 테스트를 자동화하고, 코드 품질을 체계적으로 관리하는 데 유용하다.

GTest가 제공하는 핵심 기능은 다음과 같다.

* **TEST, TEST\_F 매크로:** 직관적인 매크로를 사용하여 테스트 케이스를 작성한다.
* **어설션(Assertion):** `EXPECT_EQ`, `ASSERT_EQ` 등 다양한 형태의 어설션 함수를 제공한다.
* **SetUp, TearDown 메서드:** 테스트 전후로 사전/사후 작업을 수행할 수 있다.
* **수많은 내장 매처:** 문자열, 배열, 예외, Floating point 비교 등 세밀한 결과 검증이 가능하다.

#### ROS 2 프로젝트에서 GTest 환경 설정하기

ROS 2 Humble 프로젝트에서 GTest를 사용하기 위해서는 CMakeLists.txt 파일과 패키지 의존성 설정이 필요하다. 예시로, ROS 2의 표준 빌드 도구인 `colcon`을 사용한다고 가정해 보자.

**package.xml에서 의존성 추가**: 테스트 의존성으로 `ament_cmake_gtest`를 설정한다. 예:

```xml
<test_depend>ament_cmake_gtest</test_depend>
```

**CMakeLists.txt에서 테스트 빌드 구성**: 예시 CMakeLists.txt:

```cmake
cmake_minimum_required(VERSION 3.8)
project(my_gtest_example)

find_package(ament_cmake REQUIRED)
find_package(ament_cmake_gtest REQUIRED)

ament_add_gtest(my_test
  test/test_example.cpp
)
if(TARGET my_test)
  target_include_directories(my_test PUBLIC
    ${PROJECT_SOURCE_DIR}/include
  )
endif()

ament_package()
```

**colcon test로 테스트 실행**: 빌드 및 테스트는 다음과 같이 실행한다.

```bash
colcon build --packages-select my_gtest_example
colcon test --packages-select my_gtest_example
```

#### 단위 테스트 구조

GTest를 이용해 단위 테스트를 작성할 때 기본 단위는 `TEST(TestSuiteName, TestName)` 매크로다. 예를 들어, 다음과 같이 두 수의 합을 검증하는 테스트를 작성할 수 있다.

```cpp
#include <gtest/gtest.h>

int add(int a, int b) {
  return a + b;
}

TEST(AdditionTest, TwoPlusThreeEqualsFive) {
  EXPECT_EQ(add(2, 3), 5);
}

TEST(AdditionTest, NegativeValue) {
  EXPECT_EQ(add(-1, 5), 4);
}
```

`TEST(AdditionTest, TwoPlusThreeEqualsFive)`에서

* **AdditionTest**: 테스트 스위트(Test Suite)의 이름
* **TwoPlusThreeEqualsFive**: 해당 테스트 케이스(Test Case)의 이름

위와 같이 매크로 기반으로 테스트 케이스를 정의하면, `colcon test` 명령어 실행 시 자동으로 `AdditionTest` 스위트에 속한 모든 테스트가 수행된다.

#### SetUp과 TearDown 활용

테스트 대상 함수나 클래스에 대한 복잡한 초기화 과정이 필요한 경우, `TEST_F`(Test Fixture) 매크로를 사용하면 용이하다. C++ 기반 클래스에 `SetUp()`과 `TearDown()` 메서드를 정의하면, 각 테스트 케이스 수행 전후로 자동으로 호출되어 반복되는 초기화/정리 코드를 관리할 수 있다.

예를 들어, 행렬(또는 벡터) 연산을 테스트한다고 하자. $\mathbf{x} = \begin{bmatrix} x\_1 \ x\_2 \ x\_3 \end{bmatrix}$ 에 대해 특정 연산 함수를 테스트하려는 경우 다음과 같이 구성할 수 있다.

```cpp
#include <gtest/gtest.h>
#include <vector>

class MatrixTest : public ::testing::Test {
protected:
  std::vector<double> x;

  void SetUp() override {
    x = {1.0, 2.0, 3.0};
  }

  void TearDown() override {
    // 필요 시 자원 정리
  }
};

double sumVector(const std::vector<double>& v) {
  double s = 0.0;
  for (auto& val : v) {
    s += val;
  }
  return s;
}

TEST_F(MatrixTest, SumVector) {
  EXPECT_DOUBLE_EQ(sumVector(x), 6.0);
}
```

이렇게 `MatrixTest`라는 클래스에 테스트 관련 멤버 변수와 초기화 로직을 넣고, `TEST_F` 매크로로 테스트를 작성하면 가독성이 좋아지고 유지보수가 쉬워진다.

#### 파라미터화 테스트 활용

테스트 케이스에 다양한 입력 파라미터를 적용해보면서, 코드가 모든 입력 조건에서 제대로 동작하는지 확인하고 싶을 때가 있다. 예를 들어, 벡터 합을 계산하는 함수를 테스트하려 할 때 여러 크기, 여러 값의 벡터로 동작 여부를 검사하는 경우다. 이럴 때 GTest의 **파라미터화 테스트(Parameterized Test)** 기능을 사용하면, 반복되는 테스트 코드를 줄이고 가독성을 높일 수 있다.

**파라미터화 테스트 구조**

GTest에서 파라미터화 테스트를 작성하기 위해서는 다음 단계를 거친다.

1. **테스트 클래스를 템플릿화** `::testing::TestWithParam<T>` 클래스를 상속받는다. 여기서 `T`는 테스트에 사용될 파라미터 타입이다.
2. **테스트 스위트 등록** `INSTANTIATE_TEST_SUITE_P` 매크로를 사용해, 적용할 파라미터 값을 지정한다.
3. **TEST\_P 매크로로 테스트 정의** `TEST_F`가 아닌 `TEST_P`를 사용해 테스트 내용을 작성한다.

다음 예시는 여러 벡터의 길이를 파라미터로 하여, 그 길이에 맞춰 1부터 시작하는 양의 정수를 할당하고 합을 계산한 결과를 검증하는 상황을 가정해보자.

```cpp
#include <gtest/gtest.h>
#include <vector>
#include <numeric>

class SumVectorParameterizedTest : public ::testing::TestWithParam<size_t> {
protected:
  // 테스트마다 SetUp()이 필요하면 여기서 override 가능
};

static double sumVector(const std::vector<double>& v) {
  return std::accumulate(v.begin(), v.end(), 0.0);
}

TEST_P(SumVectorParameterizedTest, CheckSum) {
  // Given
  size_t len = GetParam();
  std::vector<double> v(len);
  for (size_t i = 0; i < len; ++i) {
    v[i] = static_cast<double>(i + 1);
  }

  // When
  double result = sumVector(v);

  // Then
  // 1부터 len까지의 합: (len * (len + 1)) / 2
  double expected = (static_cast<double>(len) * (len + 1)) / 2.0;
  EXPECT_DOUBLE_EQ(result, expected);
}

INSTANTIATE_TEST_SUITE_P(
  SumVectorTests,
  SumVectorParameterizedTest,
  ::testing::Values(
    0UL, 1UL, 2UL, 5UL, 10UL, 100UL
  )
);
```

위 코드에서 핵심 포인트는 다음과 같다.

* `SumVectorParameterizedTest`는 `::testing::TestWithParam<size_t>`를 상속받아서, `size_t` 타입의 파라미터를 받을 수 있도록 했다.
* `INSTANTIATE_TEST_SUITE_P` 매크로를 통해 여러 개의 벡터 길이(0, 1, 2, 5, 10, 100)를 지정해준다.
* `TEST_P` 매크로 내부에서 `GetParam()`으로 길이를 가져와 테스트를 실행한다.

테스트 실행 시 GTest는 지정된 모든 파라미터에 대해 같은 테스트 함수를 반복 실행하여 결과를 자동으로 검증한다.

#### 여러 파라미터 타입 사용하기

파라미터화 테스트는 단일 타입뿐 아니라 여러 개의 값을 한꺼번에 파라미터화할 수도 있다. 예를 들어, $(\mathbf{a}, \mathbf{b})$ 형태로 두 벡터를 받아서 내적(Inner Product) 함수가 올바른 값을 내는지 검사하려면 `std::tuple<std::vector<double>, std::vector<double>, double>` 같은 형태로 정의가 가능하다.

예시로, 내적 함수 $\mathbf{a} \cdot \mathbf{b}$를 테스트하려고 한다면 다음과 같이 작성할 수 있다.

```cpp
#include <gtest/gtest.h>
#include <vector>
#include <numeric>
#include <tuple>

double innerProduct(const std::vector<double>& a, const std::vector<double>& b) {
  double result = 0.0;
  for (size_t i = 0; i < a.size(); ++i) {
    result += a[i] * b[i];
  }
  return result;
}

class InnerProductTest : public ::testing::TestWithParam<
  std::tuple<std::vector<double>, std::vector<double>, double>> {
};

TEST_P(InnerProductTest, CheckInnerProduct) {
  // Given
  auto [vecA, vecB, expected] = GetParam();

  // When
  double result = innerProduct(vecA, vecB);

  // Then
  EXPECT_DOUBLE_EQ(result, expected);
}

INSTANTIATE_TEST_SUITE_P(
  VectorPairs,
  InnerProductTest,
  ::testing::Values(
    std::make_tuple(std::vector<double>{1, 2, 3}, std::vector<double>{4, 5, 6}, 32.0),
    std::make_tuple(std::vector<double>{0, 0, 0}, std::vector<double>{1, 2, 3}, 0.0),
    std::make_tuple(std::vector<double>{-1, 2}, std::vector<double>{1, 1}, 1.0)
  )
);
```

위 예시에서 `::testing::Values`로 다양한 벡터 쌍 및 기대 결과(32.0, 0.0, 1.0)를 함께 전달했다. 테스트 함수에서는 `GetParam()`을 통해서 이를 `std::tie` 혹은 구조화된 바인딩(위 예시처럼 `auto [vecA, vecB, expected]`)으로 분해해 사용한다.

파라미터화 테스트는 다음과 같은 장점을 제공한다.

* 반복되는 테스트 코드를 최소화하며 여러 값들을 한 번에 검증
* 입력 범위가 넓은 경우, 잘게 쪼개어 다양한 케이스를 쉽고 명확하게 관리

#### 다양한 어설션과 Matcher

GTest는 단위 테스트를 진행하며 다양한 상황을 정확히 검증할 수 있도록 풍부한 어설션 함수를 제공한다. 대표적인 어설션들은 다음과 같다.

* **ASSERT\_EQ(expected, actual)** / **EXPECT\_EQ(expected, actual)** 두 값이 같은지 비교한다. ASSERT 계열은 실패 시 이후 로직을 수행하지 않고 테스트를 즉시 중단한다.
* **ASSERT\_NE(expected, actual)** / **EXPECT\_NE(expected, actual)** 두 값이 다른지 비교한다.
* **ASSERT\_LT(val1, val2)**, **ASSERT\_LE(val1, val2)**, **ASSERT\_GT(val1, val2)**, **ASSERT\_GE(val1, val2)** 두 값의 대소관계를 비교한다.
* **EXPECT\_FLOAT\_EQ(val1, val2)**, **EXPECT\_DOUBLE\_EQ(val1, val2)** 부동소수점(실수) 비교 시 권장되는 어설션으로, 오차 범위를 고려하여 비교한다.
* **EXPECT\_NEAR(val1, val2, abs\_error)** 부동소수점 값의 오차 한계를 구체적으로 설정할 때 사용하는 어설션이다.
* **ASSERT\_TRUE(condition)** / **EXPECT\_TRUE(condition)** 조건이 참인지 확인한다.
* **ASSERT\_FALSE(condition)** / **EXPECT\_FALSE(condition)** 조건이 거짓인지 확인한다.

**Custom Matcher**

위와 같은 기본 어설션 이외에도, GTest는 Matcher(매처)를 활용하여 보다 유연하고 가독성 높은 검증 로직을 작성할 수 있다. C++에서 Matcher는 Google Mock 라이브러리(googlemock)에 포함되어 있으며, `#include <gmock/gmock.h>`를 통해 사용할 수 있다.

예를 들어 `std::string`에 특정 패턴이 포함되어 있는지, 크기가 특정 범위에 있는지를 간단한 한 줄의 코드로 표현 가능하다. 간단히 문자열에 대한 예시를 보자.

```cpp
#include <gtest/gtest.h>
#include <gmock/gmock.h>
#include <string>

using ::testing::HasSubstr;
using ::testing::StartsWith;
using ::testing::EndsWith;

TEST(StringTest, SubstringMatchers) {
  std::string text = "Hello ROS2 Humble";

  EXPECT_THAT(text, HasSubstr("ROS2"));
  EXPECT_THAT(text, StartsWith("Hello"));
  EXPECT_THAT(text, EndsWith("Humble"));
}
```

* **EXPECT\_THAT(actual, Matcher)** 구문을 사용하여, `text`라는 실제 결과값이 해당 Matcher 규칙을 만족하는지 검사한다.
* **HasSubstr("ROS2")**: 문자열 내에 `"ROS2"`라는 서브스트링이 존재하는지 검사한다.
* **StartsWith("Hello")**, **EndsWith("Humble")**: 문자열이 `"Hello"`로 시작하고 `"Humble"`로 끝나는지 확인한다.

이처럼 Matcher를 활용하면, 단순 비교 이상의 복합 조건을 간결하게 표현할 수 있다.

#### 예외 처리와 Death Test

ROS 2 Humble 또는 일반 C++ 환경에서 런타임 시 함수가 예외를 던지거나, 잘못된 인자 전달로 인해 강제 종료(SIGABRT, SIGSEGV 등)되는 경우를 대비한 테스트도 중요하다.

**예외 검사**: 예외가 발생했는지 여부 또는 예외 타입이 올바른지 확인하기 위해, GTest는 다음의 매크로를 제공한다.

```cpp
EXPECT_THROW(statement, ExceptionType);
EXPECT_ANY_THROW(statement);
EXPECT_NO_THROW(statement);
```

예를 들어, 다음과 같이 잘못된 벡터 크기로 예외를 던지는 함수를 테스트할 수 있다.

```cpp
#include <gtest/gtest.h>
#include <stdexcept>
#include <vector>

void checkVectorSize(const std::vector<int>& v) {
  if (v.size() < 2) {
    throw std::invalid_argument("Vector size must be >= 2");
  }
}

TEST(ExceptionTest, ThrowInvalidArgument) {
  std::vector<int> v{1};
  EXPECT_THROW(checkVectorSize(v), std::invalid_argument);
}
```

**Death Test**: 잘못된 인자로 인해 프로그램이 abort 혹은 exit로 종료되는 시나리오(예: `assert(false)`에 걸린 경우)를 검사하려면, Death Test 기능을 사용한다. GTest에서 Death Test는 다음과 같은 매크로로 제공된다.

```cpp
EXPECT_DEATH(statement, regex);
ASSERT_DEATH(statement, regex);
```

여기서 `statement`는 프로그램을 비정상 종료시킬 코드를 나타내고, `regex`는 해당 시점에 출력되는 에러 메시지를 정규 표현식으로 매칭할 수 있다(필요 없으면 `""` 등으로 비워둘 수 있다).

예외나 Death Test는 일반 어설션과 달리 시스템 자원을 해칠 가능성이 있으므로, 테스트 환경이나 CI(Continuous Integration) 설정에 따라 활용 방법을 조정해야 한다.

#### Mocking과 의존성 분리

테스트 대상을 충분히 검증하기 위해서는, 실제로 종속된 다른 컴포넌트나 외부 자원(네트워크, 파일시스템, 센서 등)이 아닌 **Mock 객체**를 사용하여 테스트해야 할 때가 많다. 예를 들어, 센서 데이터가 특정 형식으로 들어오거나, 함수가 특정 조건에서 외부 서비스를 호출한다면 이를 실제 환경에서 그대로 테스트하기는 어렵다. 이때 **Google Mock** (googlemock)을 활용하여, 의존성을 분리하고 가짜 객체(Mock)를 주입함으로써 테스트에만 집중할 수 있다.

**Google Mock 개요**

* 헤더는 `<gmock/gmock.h>`를 포함한다.
* 실제 구현이 아닌 **순수 가상 함수** 인터페이스(Interface)에 대한 Mock 클래스를 작성하여, 원하는 시나리오대로 동작을 시뮬레이션할 수 있다.
* 함수 호출 횟수, 전달받은 인자 등이 예상과 일치하는지 확인하거나, 호출 자체를 특정 결과로 대체할 수 있다.

**Mock 예시**

아래 예시는 ROS 2 내에서 메시지를 발행(publish)하는 부분을 테스트한다고 가정하자. 실제로 토픽을 발행하여 네트워크 레이어를 거치기보다는, Mock Publisher를 만들어 로직만 검증해볼 수 있다.

인터페이스를 단순화하여 다음과 같이 `IPublisher`라는 인터페이스를 정의한다고 해보자:

```cpp
// ipublisher.hpp
#ifndef IPUBLISHER_HPP
#define IPUBLISHER_HPP

#include <string>

class IPublisher {
public:
  virtual ~IPublisher() = default;
  virtual void publish(const std::string & msg) = 0;
};

#endif // IPUBLISHER_HPP
```

이를 Mock 객체로 구현하면 다음과 같이 작성할 수 있다.

```cpp
// mock_publisher.hpp
#ifndef MOCK_PUBLISHER_HPP
#define MOCK_PUBLISHER_HPP

#include <gmock/gmock.h>
#include "ipublisher.hpp"

class MockPublisher : public IPublisher {
public:
  MOCK_METHOD(void, publish, (const std::string & msg), (override));
};

#endif // MOCK_PUBLISHER_HPP
```

* `MOCK_METHOD(void, publish, (const std::string & msg), (override));`
* 반환 타입, 메서드 이름, 인자 타입을 지정해준다.
* `override`로 인터페이스에서 정의한 가상 함수를 재정의함을 명시한다.

이제 이 MockPublisher를 사용하여 실제 발행 로직을 테스트하는 코드를 작성한다.

```cpp
#include <gtest/gtest.h>
#include <gmock/gmock.h>
#include "mock_publisher.hpp"

using ::testing::_;
using ::testing::Return;
using ::testing::Exactly;

// 실제 발행 로직이 있다고 가정. 단순화된 예시:
void performPublish(IPublisher* publisher, const std::string & content) {
  // 예: 특정 조건에서만 publish
  if (!content.empty()) {
    publisher->publish(content);
  }
}

TEST(PublishTest, PublishIfNotEmpty) {
  MockPublisher mock_pub;

  // 기대: 정확히 한 번 publish가 호출되어야 하며,
  //       그 인자는 "TestMessage" 여야 한다.
  EXPECT_CALL(mock_pub, publish("TestMessage"))
    .Times(Exactly(1));

  // When
  performPublish(&mock_pub, "TestMessage");

  // Then
  // GMock에 의해 호출 횟수와 인자가 자동으로 검증됨
}

TEST(PublishTest, DoNotPublishIfEmpty) {
  MockPublisher mock_pub;

  // 기대: 빈 문자열에 대해서는 publish가 호출되지 않아야 한다.
  EXPECT_CALL(mock_pub, publish(_))
    .Times(0);

  // When
  performPublish(&mock_pub, "");

  // Then
  // 기대대로 publish가 호출되지 않았는지 GMock에서 체크
}
```

* **EXPECT\_CALL**: Mock 객체의 특정 메서드가 예상대로 호출되는지 검증한다.
* `(_ )`: 임의의 인자(any argument)를 의미하는 **Matcher**.
* `.Times(Exactly(1))`: 해당 메서드가 정확히 1회 호출되어야 한다고 명시.
* `.Times(0)`: 전혀 호출되지 않아야 함을 의미.

이처럼 Mock 객체를 통해 실제 Publisher나 외부 의존성을 직접 건드리지 않고도, **발행 로직 그 자체**를 유닛 테스트할 수 있다. ROS 2나 다른 시스템 전역의 자원을 직접 사용하지 않으므로, 테스트 수행이 가볍고 빠르며, 외부 환경으로 인한 불확실성도 낮출 수 있다.

#### 코드 커버리지 측정

단위 테스트가 충분히 작성되어 있는지를 객관적으로 확인하려면, \*\*커버리지(coverage)\*\*를 측정할 수 있어야 한다. C++ 환경에서 일반적으로 `gcov`/`lcov`와 같은 도구 혹은 `llvm-cov` 등을 사용하여 테스트 수행 시 실제로 실행된 코드 라인의 비율(라인 커버리지), 분기문 검사(분기 커버리지), 조건 검사(조건 커버리지) 등을 분석할 수 있다.

ROS 2 Humble 환경에서 커버리지를 간단히 측정하려면 다음 단계를 참고할 수 있다(표준 GCC toolchain 기준).

CMake 설정: 테스트 빌드를 할 때, 컴파일러 플래그에 `-g -O0 --coverage` 등의 옵션을 추가한다.

```cmake
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fprofile-arcs -ftest-coverage")
```

빌드 및 테스트 실행:

```bash
colcon build --packages-select my_gtest_example
colcon test --packages-select my_gtest_example
```

결과 수집:

테스트 실행 후 `.gcda`, `.gcno` 파일이 생성되며, 이를 `lcov`, `genhtml` 등으로 가공하여 HTML 리포트를 생성할 수 있다.

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

````

이 과정을 거치면 `coverage_report` 디렉터리에 HTML 형식으로 어떤 소스 코드가 어느 정도 테스트되었는지 시각적으로 확인 가능하다.

### Test Fixture를 통한 재사용성 극대화

앞서 언급했듯이 `TEST_F`를 사용하면, 클래스 기반으로 **SetUp()** 및 **TearDown()**을 정의해 반복되는 테스트 준비 로직을 재활용할 수 있다. Mocking도 이 Test Fixture와 결합하면 훨씬 깔끔한 구조를 얻을 수 있다. 예시:

```cpp

class MyPublisherFixture : public ::testing::Test {
protected:
  MockPublisher mock_pub;

  void SetUp() override {
    // 테스트 전에 필요한 준비 로직
  }

  void TearDown() override {
    // 테스트 후 정리 로직
  }
};

TEST_F(MyPublisherFixture, PublishNonEmpty) {
  EXPECT_CALL(mock_pub, publish("Something"))
    .Times(Exactly(1));

  performPublish(&mock_pub, "Something");
}

TEST_F(MyPublisherFixture, DoNotPublishEmpty) {
  EXPECT_CALL(mock_pub, publish(_))
    .Times(0);

  performPublish(&mock_pub, "");
}

````

같은 Mock 객체(`mock_pub`)를 공유하며, 각 테스트 케이스에서 중복 없이 SetUp/TearDown 처리를 수행한다.

Test Fixture를 사용하면 테스트 케이스 개수가 늘어날 때도 전체 구조가 유지보수하기 쉬워진다.

#### ROS 2 노드 단위 테스트

단순 함수나 클래스를 테스트하는 것만으로는 실제 ROS 2 노드의 동작을 충분히 확인하기 어렵다. 특히 ROS 2 노드는 **Publisher**, **Subscriber**, **Service**, **Action** 등 여러 통신 요소를 통해 서로 상호작용하기 때문이다. 이때에도 GTest를 활용해 노드 단위 테스트를 작성할 수 있다.

ROS 2의 노드 테스트를 진행하는 대표적인 방식은 다음과 같다.

* **rclcpp::Node**를 기반으로 테스트 대상 노드를 만든다.
* **rclcpp::Node::create\_publisher**나 **create\_subscription** 등으로 통신 객체를 초기화한다.
* 테스트 대상 노드에서 메시지 발행, 콜백 로직 등을 수행한다.
* 실제 메시지가 발행되었는지(혹은 수신되었는지) 확인하기 위해, 별도의 MockSubscriber나 임시 Subscriber를 만든다.
* 필요한 경우, ROS 2 테스트용 헬퍼 라이브러리(예: `rclcpp::executors`, `rclcpp::spin_some`) 등을 사용하여 콜백을 적절히 호출해준다.

아래 예시는 단순화를 위해, 노드에서 특정 토픽에 문자열을 발행하는 로직을 GTest로 확인하는 시나리오를 보여준다.

```cpp

#include <gtest/gtest.h>
#include <rclcpp/rclcpp.hpp>
#include <std_msgs/msg/string.hpp>

class SimplePublisherNode : public rclcpp::Node {
public:
  SimplePublisherNode() : Node("simple_pub_node") {
    pub_ = this->create_publisher<std_msgs::msg::String>("chatter", 10);
  }

  void publishMessage(const std::string & msg) {
    std_msgs::msg::String message;
    message.data = msg;
    pub_->publish(message);
  }

private:
  rclcpp::Publisher<std_msgs::msg::String>::SharedPtr pub_;
};

TEST(NodeTest, PublishString) {
  // Given
  auto node = std::make_shared<SimplePublisherNode>();
  auto rclcpp_context = rclcpp::Context::make_shared();
  rclcpp_context->init(0, nullptr);

  // Executor나 MultiThreadedExecutor 등 선택 가능
  auto executor = std::make_shared<rclcpp::executors::SingleThreadedExecutor>();
  executor->add_node(node);

  std::string received_data;
  auto sub_node = std::make_shared<rclcpp::Node>("sub_node", rclcpp_context);
  auto sub = sub_node->create_subscription<std_msgs::msg::String>(
    "chatter", 10,
    [&received_data](const std_msgs::msg::String::SharedPtr msg) {
      received_data = msg->data;
    }
  );
  executor->add_node(sub_node);

  // When
  std::string test_string = "Hello ROS2";
  node->publishMessage(test_string);

  // spin_some을 여러 번 호출하거나, sleep을 주어 콜백 실행 보장
  for (int i = 0; i < 10; ++i) {
    executor->spin_some(std::chrono::milliseconds(50));
  }

  // Then
  EXPECT_EQ(received_data, test_string);

  // 정리
  executor->cancel();
  rclcpp_context->shutdown();
}

```

* \*\*테스트 대상 노드(SimplePublisherNode)\*\*는 내부에서 `"chatter"`라는 토픽에 문자열 메시지를 발행한다.
* \*\*테스트 케이스(NodeTest, PublishString)\*\*에서:
  * `std::make_shared<SimplePublisherNode>()`로 노드를 생성한다.
  * 테스트 전용 **Executor**를 만들고, 테스트 대상 노드와 임시 Subscriber 노드를 등록한다.
  * `publishMessage`로 메시지를 발행한 뒤, `executor->spin_some`을 여러 번 호출해 Subscriber가 콜백을 실행할 시간을 준다.
  * 발행된 메시지가 정상적으로 Subscriber에 도착했는지(`received_data`)를 확인한다.

이와 같이 GTest + ROS 2 노드 객체를 함께 사용할 경우, 주의할 점은 다음과 같다.

* **실시간성, 멀티스레딩 이슈**: ROS 2 노드 테스트는 메시지가 전달되는 과정을 spin 또는 spin\_some으로 돌려줘야 한다. 타이밍 문제로 인해 테스트가 불안정해지지 않도록 충분한 시간을 주거나, 혹은 조건 변수를 사용해 콜백이 완료되었음을 정확히 기다리는 방법을 고려한다.
* **Context, Executor 관리**: 테스트마다 별도의 `rclcpp::Context`를 사용하면 서로 다른 테스트 케이스가 동시에 구동될 때 충돌을 방지할 수 있다.
* **Fixture 활용**: 여러 노드나 Subscriber 설정이 필요한 경우, `TEST_F`를 사용하여 SetUp에서 노드 및 Executor 초기화를 재활용할 수 있다.

#### 동시성(Concurrency) 테스트

C++/ROS 2 환경에서는 멀티스레딩과 비동기 콜백이 빈번히 사용된다. 단위 테스트에서도 동시성 이슈를 검증하거나, 특정 스레드에서 안전하게 동작하는지 확인해야 할 때가 있다.

* **Race Condition 테스트**: 예를 들어, 공용 자원을 두 스레드가 동시에 접근하는 상황을 GTest로 재현하고, 예기치 않은 결과가 발생하지 않는지 확인할 수 있다.
* **Thread Sanitizer** 등 추가 도구와 결합하면, 데이터 레이스나 교착 상태 등을 포착하는 데 도움이 된다.

단, GTest 자체는 스레드 안전 문제가 없도록 설계되었으나, 테스트 코드 내부에서 잘못된 동시 접근이 일어나면 예측하기 힘든 결과가 나올 수 있다. 따라서 스레드 관련 테스트는 충분한 로그와 제한된 실행 시간을 설정해두고 수행하는 것이 바람직하다.

#### TDD(Test-Driven Development)와 GTest

ROS 2 프로젝트를 TDD 관점에서 진행할 때, GTest는 “테스트 코드 먼저 작성하고, 이후 기능 구현”이라는 흐름을 자연스럽게 지원해준다.

1. **테스트 케이스부터 작성**: 어떤 기능(예: 메시지 처리 로직)을 구현하기 전, 그 함수나 노드에서 기대하는 동작을 정의하는 GTest 케이스를 먼저 작성한다.
2. **실패하는 테스트 확인**: 초기에는 당연히 기능이 구현되지 않았으므로 테스트가 실패한다.
3. **기능 구현**: 필요한 최소한의 로직을 작성하여 테스트가 통과하도록 만든다.
4. **리팩터링**: 중복 제거, 구조 개선 등을 수행하면서도, 기존 테스트 케이스를 돌려 기능이 여전히 정상 동작하는지 보장한다.

이 과정을 여러 번 반복하면, 테스트와 기능 구현이 자연스럽게 맞물려서 코드 품질을 높일 수 있다.

#### CI(Continuous Integration) 환경에서 GTest 활용

CI(지속적 통합)를 통해 코드 변경 시마다 자동으로 빌드와 테스트를 수행하면, 품질을 안정적으로 유지할 수 있다. ROS 2 프로젝트에서도 GitHub Actions, GitLab CI, Jenkins 등 다양한 CI 도구를 사용할 수 있으며, GTest 단위 테스트를 CI 파이프라인에 연동해 자동화하는 패턴이 일반적이다.

**예시: GitHub Actions**

ROS 2 Humble 프로젝트에서 GitHub Actions를 사용한다고 가정하면, 대략 다음과 같은 Workflow YAML 파일을 구성할 수 있다.

```yaml

name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2

    - name: Setup ROS 2 Humble
      uses: ros-tooling/setup-ros@v0.2
      with:
        required-ros-distributions: humble

    - name: Install dependencies
      run: |
        sudo apt-get update
        rosdep update
        rosdep install --from-paths src --ignore-src -y

    - name: Build
      run: |
        colcon build --symlink-install

    - name: Test
      run: |
        colcon test
        colcon test-result --verbose

```

* `actions/checkout@v2`: 소스 코드를 가져오는 역할
* `ros-tooling/setup-ros@v0.2`: GitHub Actions 환경에 ROS 2 Humble을 세팅
* `colcon test`: GTest를 포함한 모든 테스트를 실행
* `colcon test-result --verbose`: 어떤 테스트가 성공/실패했는지 요약을 확인

**예시: Jenkins**

Jenkins에서도 **Freestyle Project** 혹은 **Pipeline** 스크립트로 동일한 단계를 구성할 수 있다.

1. **소스 코드 체크아웃**
2. **ROS 2 Humble 환경 설정** (예: Docker 컨테이너 사용, 혹은 사전에 설치된 ROS 2 환경)
3. **빌드 & 테스트**: `colcon build`, `colcon test`
4. **결과 수집**: JUnit XML 결과로 변환하여 Jenkins에서 테스트 리포트 생성

이를 통해, Pull Request(혹은 MR)마다 자동으로 GTest 단위 테스트가 실행되고, 실패 시 즉각 피드백을 받을 수 있다.

#### 테스트 코드 구성 팁

* **테스트는 한 번에 하나의 로직만 검증하라** 여러 기능을 한 테스트 케이스에 몰아넣으면 실패 시 원인을 파악하기 어렵다.
* **네이밍 컨벤션** `TEST(SuiteName, CaseName)`에서 SuiteName과 CaseName이 명확히 어떤 기능을 테스트하는지 드러나도록 짓는다.
* **중복 최소화** 반복되는 테스트 준비 로직이 있다면 `TEST_F` 혹은 헬퍼 함수를 사용한다.
* Mock과 Stub 구분
  * Mock: 호출 횟수/인자를 검증할 필요가 있을 때
  * Stub: 단순히 임의의 반환값만 필요할 때
* **실패 메세지 활용** 어설션이 실패했을 때 어떤 입력, 어떤 상황에서 실패했는지 알기 쉽게 설명하라.
* **테스트 실행 속도 주의** ROS 2 노드 테스트나 Mock 기반 테스트 모두, 수백\~수천 개의 케이스를 빠르게 돌릴 수 있도록 가능하면 가벼운 로직만 테스트 케이스에 남기고, 무거운 작업은 분리한다.

#### 정리

위와 같이, GTest를 활용하면 C++ 기반 ROS 2 Humble 프로젝트 전반에 걸쳐 단위 테스트를 확실하게 구성할 수 있다. 함수 단위, 클래스 단위, 노드 단위, 그리고 Mocking을 통한 외부 의존성 격리 등 다양한 상황에서 테스트를 자동화함으로써 코드 품질을 높은 수준으로 유지할 수 있다.
