AEGLOSLAB ROS 2 Jazzy 대전

제14부 시스템 설계·품질·운영

14.5 테스트 I — 단위·모의 그래프

14.5.1 이 장에서 배우는 것 / 선수 지식

로봇 소프트웨어 시험이 어려운 이유는 분명하다. 하드웨어가 필요하고, 물리 세계와 상호작용하고, 비결정적이고(12.5), 실패가 물리적 피해로 이어진다. 그래서 "시험이 어렵다"를 이유로 시험 없이 개발하다가, 현장에서 문제를 만나는 순환이 흔하다.

그런데 이 어려움의 상당 부분은 설계로 해결된다. 12.5.8에서 말한 "핵심 알고리즘을 순수 함수로 분리" 가 그 핵심이다. 계산부가 ROS와 무관하면 완전히 결정적으로 시험할 수 있고, ROS 계층은 별도로 다룬다.

배우는 것:

  • 시험 층위 — 무엇을 어디서 시험하는가.
  • 시험 가능한 설계 — 분리의 원칙.
  • 단위 시험 — 순수 로직.
  • 노드 시험 — ROS 인터페이스.
  • 모의 그래프 — 가짜 발행자·구독자.
  • 시간 다루기 — 시험에서의 시계.
  • 비결정성 — 안정된 시험 만들기.
  • 계측을 시험으로(14.5.11) — 할당·동시성·주기·TSAN을 회귀 단언으로 굳히는 법.
  • 그 단언을 실제로 실행한 결과(14.5.12) — 통과와 실패를 둘 다 확인하는 법.

선수 지식: 4.5(빌드·colcon test), 4.10(CI), 12.5(결정성), 14.1(노드 분할), 6.2.8(mock 하드웨어), 14.6(통합 시험 — 다음 장).

정직: 시험 프레임워크의 API·매크로 이름은 배포판·언어에 따라 다르다(확인 필요). 이 장은 전략과 패턴을 다루며, 실제 코드는 전형적 형태다. 이 환경에서 실행 검증하지 않았다(목차 3). 다만 14.5.11의 회귀 단언은 실제 패키지로 colcon test까지 실행했다(실행 확인 — 14.5.12).

14.5.2 시험 층위

시험 층위 ① 순수 로직 ROS 없음 · 빠름 · 결정적 ② 노드 단위 ROS 인터페이스 ③ 모의 그래프 여러 노드 · 가짜 데이터 ④ 통합 · launch 14.6 ⑤ 시뮬 10.1~10.6 ⑥ 실기 · HIL 14.6
층속도신뢰도개수
① 순수매우 빠름로직만많이
② 노드빠름인터페이스많이
③ 모의 그래프중간상호작용중간
④ 통합느림구성적게
⑤ 시뮬느림동작적게
⑥ 실기매우 느림최종최소

견해: 아래로 갈수록 신뢰도가 높지만 비용도 크다는 고전적 피라미드가 로봇에서 더 극단적이다. 실기 시험은 사람·장비·시간이 들고 위험도 있다. 아래에서 검출할 수 있는 것을 위에서 검출하지 말라.

견해: ①의 비중을 높이는 것이 전체 시험 전략을 좌우한다. 순수 로직 시험은 초 단위로 실행되고 결정적이라 CI에 부담이 없다. 여기서 검출되는 버그를 늘리는 것이 곧 개발 속도다.

14.5.3 시험 가능한 설계

시험이 어려운 코드는 대개 설계 문제다.

✗ 시험 어려움 ✓ 시험 쉬움 콜백 안에 모든 로직 ROS · 시간 · 계산 혼재 ROS 계층 구독 · 발행 · 파라미터 순수 계산 클래스 ROS 무관
// ✓ 순수 계산부 — ROS를 모른다
class PathSmoother {
public:
  explicit PathSmoother(const Config & cfg) : cfg_(cfg) {}
  std::vector<Pose2D> smooth(const std::vector<Pose2D> & raw) const;   // 순수 함수
private:
  Config cfg_;
};

// ✓ ROS 계층 — 얇게
class SmootherNode : public rclcpp::Node {
  void on_path(const nav_msgs::msg::Path::SharedPtr msg) {
    auto raw = to_pose2d(*msg);              // 변환
    auto out = smoother_.smooth(raw);        // 계산 (시험 대상)
    pub_->publish(to_path_msg(out, msg->header));
  }
  PathSmoother smoother_;
};
분리 원칙이유
계산과 I/O 분리계산만 단위 시험
시간을 주입고정 시각으로 시험(14.5.6)
의존을 인터페이스로모의 가능
부작용 최소화예측 가능
설정을 인자로다양한 조건

견해: "콜백은 얇게, 계산은 클래스로"가 이 책이 여러 곳에서 권한 구조다(2.3.4의 성능 이유·12.5.8의 결정성 이유). 시험 용이성은 그 세 번째 이유이며, 셋이 같은 방향을 가리킨다는 점이 이 구조가 옳다는 신호다.

견해: 변환 함수(to_pose2d·to_path_msg)도 시험 가능하게 분리하라. 메시지↔내부 표현 변환에서 프레임·단위·부호 실수가 자주 나는데(7.1.6), 이것은 순수 함수라 시험이 쉽다.

14.5.4 단위 시험

// C++ — gtest (ament_cmake_gtest)
#include <gtest/gtest.h>
#include "my_pkg/path_smoother.hpp"

TEST(PathSmoother, EmptyInputReturnsEmpty) {
  PathSmoother s{Config{}};
  EXPECT_TRUE(s.smooth({}).empty());
}

TEST(PathSmoother, PreservesEndpoints) {
  PathSmoother s{Config{}};
  std::vector<Pose2D> raw = {{0,0,0}, {1,0,0}, {2,0,0}};
  auto out = s.smooth(raw);
  ASSERT_FALSE(out.empty());
  EXPECT_NEAR(out.front().x, 0.0, 1e-9);
  EXPECT_NEAR(out.back().x,  2.0, 1e-9);
}
# Python — pytest
def test_smoother_preserves_endpoints():
    s = PathSmoother(Config())
    out = s.smooth([(0,0,0), (1,0,0), (2,0,0)])
    assert out[0] == pytest.approx((0,0,0))
# CMakeLists.txt
if(BUILD_TESTING)
  find_package(ament_cmake_gtest REQUIRED)
  ament_add_gtest(test_path_smoother test/test_path_smoother.cpp)
  target_link_libraries(test_path_smoother my_pkg_lib)
endif()
colcon test --packages-select my_pkg
colcon test-result --verbose        # 결과 확인 (★ 실패를 놓치지 않으려면)

함정: colcon test는 시험이 실패해도 종료 코드가 성공일 수 있다(확인 필요: 버전별 동작). colcon test-result --verbose로 확인하는 것을 습관화하고, CI에서도 이 단계를 넣어야 한다. 이것을 빠뜨려 "시험이 통과한다고 믿었는데 아니었던" 경우가 흔하다.

로봇 도메인에서 시험할 것:

대상예
경계 조건빈 입력·단일 점·최대 크기
수치각도 랩어라운드·0 나눗셈·NaN
좌표 변환왕복 일치(5.2)
단위·부호규약 준수(7.1.6)
상태 기계모든 전이(1.12)
안전 제한클램프가 동작하나(8.7.5)
시간 처리점프·역행(1.10.4)

견해: 각도 랩어라운드가 로봇 코드의 대표적 버그다. $\pi$ 근처에서 부호가 뒤집히거나 차이가 $2\pi$가 되는 오류는 시험 없이는 놓치기 쉽다. 경계값 시험을 반드시 넣으라 — atan2 결과 비교, 각도 차이 계산 등.

견해: "왕복 시험"이 변환 코드에 효과적이다. 좌표 변환·직렬화·메시지 변환은 f(g(x)) == x를 확인하면 많은 실수가 검출된다. 작성 비용이 낮고 효과가 크다.

14.5.5 노드 시험

ROS 인터페이스를 시험한다.

// 노드를 만들고 메시지를 주입해 출력을 확인
class SmootherNodeTest : public ::testing::Test {
protected:
  void SetUp() override {
    rclcpp::init(0, nullptr);
    node_under_test_ = std::make_shared<SmootherNode>(node_options());
    helper_ = std::make_shared<rclcpp::Node>("test_helper");
    pub_ = helper_->create_publisher<nav_msgs::msg::Path>("path_in", 10);
    sub_ = helper_->create_subscription<nav_msgs::msg::Path>(
      "path_out", 10, [this](nav_msgs::msg::Path::SharedPtr m){ last_ = m; });
    exec_.add_node(node_under_test_);
    exec_.add_node(helper_);
  }
  void TearDown() override { rclcpp::shutdown(); }

  void spin_until_received(std::chrono::milliseconds timeout) {
    auto deadline = std::chrono::steady_clock::now() + timeout;
    while (!last_ && std::chrono::steady_clock::now() < deadline) {
      exec_.spin_some();
      std::this_thread::sleep_for(std::chrono::milliseconds(1));
    }
  }
  rclcpp::executors::SingleThreadedExecutor exec_;
  nav_msgs::msg::Path::SharedPtr last_;
  // ...
};

TEST_F(SmootherNodeTest, PublishesSmoothedPath) {
  pub_->publish(make_test_path());
  spin_until_received(std::chrono::seconds(2));
  ASSERT_NE(last_, nullptr) << "출력이 오지 않음";
  EXPECT_GT(last_->poses.size(), 0u);
}
시험 대상확인
토픽 이름·타입규약대로
QoS호환(1.6)
파라미터 처리기본값·검증(1.9)
경계 입력빈 메시지·비정상 값
오류 처리잘못된 입력에 종료되지 않는가
생애주기전이 동작(1.12)

함정: 노드 시험은 타이밍에 의존해 불안정해지기 쉽다. sleep 후 확인하는 방식은 느린 머신에서 실패한다(4.10.6). "조건이 만족될 때까지 타임아웃 안에서 spin" 하는 패턴을 쓰라 — 위 spin_until_received가 그것이다.

함정: rclcpp::init/shutdown을 시험마다 호출할 때 상태가 남을 수 있다(확인 필요: 정확한 동작). 시험 간 격리가 안 되면 순서에 따라 결과가 달라진다. 격리를 확인하고, 안 되면 프로세스를 분리한다.

함정: 도메인 ID를 격리하지 않으면 CI에서 병렬 실행 시 시험끼리 간섭한다(1.5.8). 같은 머신의 다른 시험이 같은 토픽을 쓰면 서로 데이터를 받는다. 시험마다 고유 ROS_DOMAIN_ID를 주거나 격리 수단을 쓰라 — CI 불안정의 흔한 원인이다.

14.5.6 시간 다루기

시험에서 시간은 통제해야 한다.

방법특성
시계 주입노드가 시계를 인자로 받음
use_sim_time + /clock 발행ROS 시간 통제(1.10)
실제 대기느리고 불안정
가짜 시계 객체순수 로직에 적합
// 순수 로직에 시간을 주입
class Watchdog {
public:
  bool is_stale(double now, double last_msg_time) const {
    return (now - last_msg_time) > timeout_;
  }
};
// 시험: 시간을 자유롭게 지정
EXPECT_TRUE(wd.is_stale(10.0, 5.0));
# 노드 시험에서 시뮬 시간 사용
node = MyNode(parameter_overrides=[Parameter('use_sim_time', value=True)])
clock_pub.publish(Clock(clock=Time(sec=100)))     # 시각을 원하는 대로

견해: "타임아웃 로직"은 시간을 주입해 시험하라. 실제로 5초를 기다리는 시험은 느리고, CI에서 불안정하다. 시간을 인자로 받는 함수로 만들면 즉시·결정적으로 시험된다. 6.9의 STALE 판정, 14.2.5의 신선도(마지막 메시지 이후 경과) 감시가 모두 이 형태다.

함정: std::this_thread::sleep_for로 시간이 흐르길 기다리는 시험은 지양하라. 시험이 느려지고, 부하가 높으면 실패한다. 불가피하면 넉넉한 타임아웃 + 조건 확인 루프를 쓴다.

14.5.7 모의 그래프

여러 노드의 상호작용을 가짜 데이터로 시험한다.

가짜 센서 발행자 정해진 시나리오 시험 대상 노드(들) 결과 수집 구독자 가짜 tf 발행자 가짜 서비스 서버 단언 검증
모의 대상방법
센서 데이터합성 메시지 발행
tfStaticTransformBroadcaster·수동 발행
시간/clock(14.5.6)
서비스 응답가짜 서버
액션가짜 액션 서버
하드웨어mock 인터페이스(6.2.8)
# 가짜 센서 시나리오 (개념)
class FakeLidar(Node):
    def __init__(self, scenario):
        super().__init__('fake_lidar')
        self.pub = self.create_publisher(LaserScan, 'scan', qos_sensor())
        self.scenario = scenario          # [(t, ranges), ...]
    def emit(self, index):
        self.pub.publish(make_scan(self.scenario[index]))

견해: "시나리오를 데이터로 정의"하면 시험이 읽기 쉬워진다. 코드로 메시지를 조립하는 대신 표·YAML로 시나리오를 두고 재생하면, 시험 추가가 쉽고 비개발자도 읽을 수 있다.

견해: 실기 bag의 일부를 시험 입력으로 쓰는 것이 강력하다(10.6.6·11.3.8). 합성 데이터는 개발자가 상상한 것만 담지만, 실제 데이터는 예상 못 한 조합을 담는다. 문제가 났던 bag을 잘라 시험 픽스처로 만들면 회귀가 확실히 검출된다.

함정: 모의 데이터가 실제와 다르면 시험이 거짓 안심을 준다(10.1.3의 시뮬 경계와 같은 구조). 가짜 라이다가 항상 잡음 없는 값을 주면, 실제의 노이즈·결측(10.3.7)을 처리하는 코드는 시험되지 않는다. 비정상 케이스를 의도적으로 넣으라.

넣어야 할 비정상 케이스:

케이스이유
NaN·inf 값센서 결측(7.1.7)
빈 메시지경계
오래된 스탬프지연·정지(7.7.7)
순서 뒤바뀜네트워크
중복 메시지재전송
급격한 값 변화튐
데이터 끊김센서 고장

견해: "센서가 고장 나면 어떻게 되는가"를 시험하라. 정상 데이터만 시험하면 이 경로는 현장에서 처음 실행된다. 데이터를 끊고 노드가 안전하게 동작하는지 확인하는 시험이 실질적 가치가 크다.

14.5.8 안정된 시험 만들기

불안정한(flaky) 시험은 시험이 없는 것보다 나쁠 수 있다 — 신뢰를 잃으면 아무도 안 본다.

불안정 원인대처
sleep 기반 타이밍조건 대기 루프
도메인 간섭고유 도메인 ID
실행 순서 의존격리·초기화
난수시드 고정(12.5.3)
부동소수 정확 비교허용 오차
외부 자원모의
CI 부하 변동넉넉한 타임아웃
포트·파일 충돌고유 이름

견해: 불안정한 시험을 발견하면 즉시 고치거나 비활성화하라. 방치하면 "가끔 빨간 것은 원래 그래"가 되어 실제 실패도 무시된다. 4.10에서 CI를 다룰 때의 원칙과 같다.

견해: 타임아웃은 넉넉하게, 대신 조건을 확인하라. 2초 안에 끝날 일에 타임아웃 10초를 줘도, 조건이 만족되면 즉시 반환하므로 정상 시에는 느려지지 않는다. 실패 시에만 10초를 기다린다.

14.5.11 계측을 시험으로 굳히기 — 회귀를 막는 네 가지 단언

11.7.13의 계측 도구는 한 번 측정하고 끝내면 값이 절반이다. 같은 코드를 시험으로 두면 누군가 성질을 바꿨을 때 CI가 알려 준다. 이 책이 실측하며 쓴 것을 그대로 단언으로 바꾼 형태다.

전제: 아래 넷은 MultiThreadedExecutor를 쓰거나 실시간 경로가 있는 패키지에 값이 있다. 그렇지 않은 패키지에 넣으면 유지 비용만 든다.

14.5.11.1 ① 할당 회귀 — 절대값이 아니라 기준선

TEST(Perf, SpinAllocationBaseline) {
  auto node = std::make_shared<rclcpp::Node>("t");
  rclcpp::executors::SingleThreadedExecutor exec;
  exec.add_node(node);
  for (int i = 0; i < 200; ++i) exec.spin_some();   // 예열

  counting = true;
  const int N = 1000;
  for (int i = 0; i < N; ++i) exec.spin_some();
  counting = false;

  EXPECT_LE(n_new / (double)N, kBaselinePerSpin);   // 예: 12
}

"0"을 목표로 두지 말라 — 기본 구성의 rclcpp는 회당 7~9회를 쓴다(12.3.12.2). 검출해야 할 것은 "우리가 뭔가를 넣었더니 9가 40이 됐다"이다.

함정: 기준선은 rclcpp 버전에 딸린 값이다. 배포판을 업그레이드하면 기준선도 다시 설정해야 한다. 상수를 코드에 박고 주석에 측정 조건(버전·빌드 타입)을 남기라.

14.5.11.2 ② 동시성 계약 — 그룹 설정이 의도대로인가

콜백 그룹은 한 줄만 바꿔도 스레드 안전성 요구 전체가 바뀐다(2.9.10.1). 그런데 그 변경은 리뷰에서 눈에 잘 띄지 않는다.

TEST(Concurrency, ControlCallbackNeverOverlaps) {
  // … 노드를 실행하고 2초 실행한 뒤 …
  EXPECT_EQ(max_inflight.load(), 1)
      << "제어 콜백이 동시에 실행됐다 — 그룹 설정을 확인하라";
}

이 단언 하나가 "누군가 Reentrant로 바꿨다"를 검출한다. 반대로 의도적으로 동시 실행이 필요한 콜백에는 EXPECT_GT(max_inflight, 1)을 두어 그 의도를 문서화할 수 있다.

14.5.11.3 ③ 주기 예산 — 꼬리로 판정한다

TEST(Timing, ControlPeriodTail) {
  // … 1500 주기를 모은 뒤 …
  EXPECT_LT(p99, kPeriodMs * 1.10)   << "p99가 목표의 110%를 넘음";
  EXPECT_LT(std::abs(누적오차_비율), 0.01) << "드리프트 1% 초과";
}

평균이 아니라 p99와 누적 오차를 본다(11.7.3·2.5.13.1). 평균은 콜백이 주기를 넘기 직전까지 아무것도 알려 주지 않는다 — 그러다 넘는 순간 주기가 곧 콜백 시간이 된다(2.5.13.4).

함정: CI 러너에서는 꼬리가 넓다(11.7.9·4.10.6). 임계를 실기 기준으로 설정하면 오탐이 대량 발생한다. CI에서는 누적 오차(드리프트)만 보고, 지터는 전용 하드웨어에서 보라 — 드리프트는 부하에 둔감하다(2.5.13.2에서 콜백이 주기의 50%를 써도 0.000%였다).

14.5.11.4 ④ TSAN — 별도 잡으로

# CI: 야간 또는 PR 검증에 한 번
- name: thread-sanitizer
  run: |
    colcon build --cmake-args -DCMAKE_BUILD_TYPE=Debug       -DCMAKE_CXX_FLAGS="-fsanitize=thread -g -O1"       -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=thread"
    TSAN_OPTIONS="halt_on_error=0" colcon test

부하를 이유로 미리 포기하지 말라 — 이 책의 측정에서는 약 2배였다(2.9.10.3). 그리고 정상 구성에서는 경고가 0건이었으므로(오탐 없음) 실패가 보고되면 실제 실패다.

견해: 넷 중 하나만 고른다면 ②(동시성 계약)다. 비용이 거의 없고, 검출해 주는 버그가 가장 비용이 크다 — 경쟁 조건은 시험에서 통과하고 현장에서 드러난다(2.9.1). 그리고 이 단언은 의도를 코드에 남기는 문서 역할도 한다.

14.5.11.5 시험이 거짓 안전을 주지 않게

14.5.8은 "불안정한 시험은 없는 것보다 나쁠 수 있다"고 했다. 계측 시험에는 반대 방향의 위험이 하나 더 있다 — 항상 통과하는 시험.

위험예방어
전제가 성립 안 해 늘 통과콜백이 짧아 애초에 안 겹치는데 max_inflight == 1을 단언전제도 단언하라 — EXPECT_GE(호출수, 최소)
기준선이 너무 느슨회당 할당 상한을 1000으로실측값의 1.3~1.5배로
측정 구간에 초기화 포함13,088회에 묻힘예열 후 재기(11.7.13.1)

첫 줄이 이 책이 실제로 겪은 것이다(2.9.10.4·11.7.13.2). "경쟁이 없다"는 결과가 사실은 "경쟁을 만들지 못했다"였다. 시험도 같은 함정에 빠진다 — 통과하는 시험이 무엇을 확인했는지 한 번은 의심해 보라.

14.5.12 그 단언들을 실제로 실행해 봤다 — 실측

14.5.11은 회귀 단언 넷을 코드로 제시했다. 실제 패키지를 만들어 colcon test로 실행했다(실행 확인, Jazzy, ament_cmake_gtest).

구성: ament_cmake 패키지 하나에 ament_add_gtest로 시험 셋. 정상 구성에서 통과하는지와 일부러 위반했을 때 실패하는지를 둘 다 확인했다 — 후자가 없으면 14.5.11.5가 경고한 "항상 통과하는 시험"이다.

14.5.12.1 정상 구성 — 셋 다 통과했고, 값이 앞선 실측과 맞았다

[ 측정 ] 회당 할당 = 9.00
[   OK ] Perf.SpinAllocationBaseline (115 ms)
[ 측정 ] 호출 99회, 최대 동시 1
[   OK ] Concurrency.ControlCallbackNeverOverlaps (1514 ms)
[ 측정 ] p99 = 10.061 ms, 드리프트 = -0.0006%
[   OK ] Timing.ControlPeriodTail (4005 ms)
[ PASSED ] 3 tests.
단언이 시험의 값앞선 회차의 값
회당 할당9.009(12.3.12.1)
최대 동시11(2.4.12.1)
p9910.061 ms10.047 ms(2.5.13.1)
드리프트−0.0006%0.000%(2.5.13.2)

다른 프로그램·다른 컨테이너·colcon/gtest 경로로 재현됐다. 앞선 실측이 실험 구성에 딸린 값이 아니라는 뜻이다.

14.5.12.2 일부러 어겼을 때 — 의도한 이유로 실패한다

단언위반 구성결과
② 동시성 계약그룹을 Reentrant로최대 동시 4 · 호출 300회 → FAIL
① 할당 기준선rclcpp::spin_some(node) 자유 함수로회당 125.00 → FAIL(회당 할당이 기준선을 넘었다)

둘 다 한 줄 수정으로 실패한다. 그리고 실패 메시지가 원인을 그대로 말해 준다.

②의 부수 관찰이 흥미롭다 — Reentrant로 바꾸자 호출 수가 99 → 300으로 늘었다. 시험이 검출한 것은 "느려졌다"가 아니라 "빨라졌는데 위험해졌다"이다. 성능 지표만 보는 회귀 시험이라면 이것을 개선으로 읽었을 것이다. 동시성은 계약으로 단언해야 검출된다.

14.5.12.3 실패가 "잘못된 이유"였던 한 번

①의 위반 구성을 처음 만들 때 테스트가 실패하긴 했는데 이유가 달랐다.

unknown file: Failure          ← EXPECT 가 아니라 예외로 종료됐다

같은 노드를 executor에 등록해 둔 채 자유 함수 spin_some(node)을 불러 Node has already been added to an executor 예외가 났던 것이다(2.4.13.4와 같은 계열의 실수).

"실패했으니 시험이 동작한다"고 넘기면 안 된다. 그 상태로 두었다면 "할당 기준선 시험이 회귀를 검출한다"고 틀린 결론을 적었을 것이다 — 실제로는 무관한 예외를 포착하고 있었다. 실패 메시지가 내가 의도한 단언에서 나왔는지 확인하라.

14.5.12.4 부수로 확인한 것 — package.xml의 이메일 검증

시험 패키지를 만들다 빌드가 이렇게 막혔다.

catkin_pkg.package.InvalidPackage: Error(s) in package '…/package.xml':
Invalid email "[email protected]" for person "book"

<maintainer>의 이메일은 형식 검증을 받는다 — TLD가 짧으면 거부된다(4.4.8). 빌드 자체가 실패하므로 조용하지 않다는 점은 다행이지만, "아무 값이나 넣고 나중에 고치자"가 통하지 않는다.

14.5.12.5 확인하지 못한 것

  • ④ TSAN 잡은 이 패키지로 실행하지 않았다 — 별도 빌드 구성이 필요하고, 탐지·부하는 2.9.10.2에서 이미 측정했다.
  • ③ 주기 예산의 위반 구성은 만들지 않았다 — 콜백을 주기보다 길게 만들면 실패할 것으로 보이나 확인하지 않았다(확인 필요).
  • CI 러너에서의 통과율은 측정하지 않았다 — 14.5.11.3이 경고한 대로 꼬리 지표는 공유 러너에서 흔들린다.

14.5.9 함정 카탈로그

#함정증상대처
1콜백에 로직 집중시험 불가계산 분리
2test-result 미확인실패를 통과로 오인항상 확인
3sleep 기반 시험불안정조건 대기
4도메인 미격리CI 간섭고유 ID
5시험 간 상태 잔존순서 의존격리
6실시간 대기로 타임아웃 시험느림·불안정시간 주입
7정상 데이터만 시험실전 실패비정상 케이스
8모의가 실제와 괴리거짓 안심bag 활용
9각도·수치 경계 미시험대표적 버그경계값 시험
10부동소수 정확 비교간헐 실패허용 오차
11난수 시드 미고정불안정고정
12불안정 시험 방치신뢰 상실즉시 대응
13상위 층에서만 시험느림·원인 불명피라미드
14QoS 시험 누락현장 미연결인터페이스 시험
15전제가 안 성립해 늘 통과하는 시험거짓 안전전제도 함께 단언(14.5.11.5)
16할당 시험의 목표를 0으로영원히 실패기준선 대비로(14.5.11.1)
17실패했으니 시험이 동작한다고 판단무관한 예외를 포착하고 있을 수 있다실패 메시지가 그 단언에서 나왔는지 확인(14.5.12.3)
18성능 지표만으로 회귀 판정Reentrant 전환은 빨라 보인다(99→300회)동시성은 계약으로 단언(14.5.12.2)

14.5.10 체크포인트

  • 시험 여섯 층위와 각각의 속도·신뢰도·개수 방침을 말할 수 있는가.
  • "아래에서 검출할 수 있는 것을 위에서 검출하지 말라"의 근거는?
  • 시험 가능한 설계의 다섯 원칙과, "콜백은 얇게"가 세 가지 이유에서 지지되는 것은?
  • colcon test 사용 시 반드시 함께 해야 할 것과 그 이유는?
  • 로봇 도메인에서 시험할 일곱 대상과, 대표적 버그 하나는?
  • "왕복 시험"이 효과적인 대상은?
  • 노드 시험에서 확인할 여섯 가지와, 안정성을 위한 패턴은?
  • CI 병렬 실행에서 시험이 간섭하는 원인과 대처는?
  • 시험에서 시간을 다루는 네 방법과, 타임아웃 로직 시험의 표준 방법은?
  • 모의 그래프의 여섯 모의 대상을 들 수 있는가.
  • "시나리오를 데이터로 정의"의 이점은?
  • 실기 bag을 시험 픽스처로 쓰는 것의 가치는?
  • 모의 데이터가 주는 거짓 안심을 피하는 방법과, 넣어야 할 비정상 케이스 일곱 가지는?
  • 불안정한 시험의 여덟 원인과, 방치하면 안 되는 이유는?
  • 타임아웃을 넉넉히 주되 조건을 확인하라는 조언의 근거는?
  • 할당 회귀 시험의 목표를 0으로 두면 안 되는 이유는(14.5.11.1).
  • 계측 시험이 "항상 통과"할 때 의심할 것은 무엇인가(14.5.11.5).
  • 회귀 단언을 새로 쓰면 무엇을 두 번 확인해야 하는가(14.5.12.2).
  • Reentrant 전환이 성능 지표에는 어떻게 보이며, 그것이 왜 위험한가(14.5.12.2).

다음 장 14.6은 그 위 층 — launch_testing·통합 시험·HIL로 시스템 전체를 검증하는 방법을 다룬다.