제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 시험 층위
| 층 | 속도 | 신뢰도 | 개수 |
|---|---|---|---|
| ① 순수 | 매우 빠름 | 로직만 | 많이 |
| ② 노드 | 빠름 | 인터페이스 | 많이 |
| ③ 모의 그래프 | 중간 | 상호작용 | 중간 |
| ④ 통합 | 느림 | 구성 | 적게 |
| ⑤ 시뮬 | 느림 | 동작 | 적게 |
| ⑥ 실기 | 매우 느림 | 최종 | 최소 |
견해: 아래로 갈수록 신뢰도가 높지만 비용도 크다는 고전적 피라미드가 로봇에서 더 극단적이다. 실기 시험은 사람·장비·시간이 들고 위험도 있다. 아래에서 검출할 수 있는 것을 위에서 검출하지 말라.
견해: ①의 비중을 높이는 것이 전체 시험 전략을 좌우한다. 순수 로직 시험은 초 단위로 실행되고 결정적이라 CI에 부담이 없다. 여기서 검출되는 버그를 늘리는 것이 곧 개발 속도다.
14.5.3 시험 가능한 설계
시험이 어려운 코드는 대개 설계 문제다.
// ✓ 순수 계산부 — 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 | StaticTransformBroadcaster·수동 발행 |
| 시간 | /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.00 | 9(12.3.12.1) |
| 최대 동시 | 1 | 1(2.4.12.1) |
| p99 | 10.061 ms | 10.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 | 콜백에 로직 집중 | 시험 불가 | 계산 분리 |
| 2 | test-result 미확인 | 실패를 통과로 오인 | 항상 확인 |
| 3 | sleep 기반 시험 | 불안정 | 조건 대기 |
| 4 | 도메인 미격리 | CI 간섭 | 고유 ID |
| 5 | 시험 간 상태 잔존 | 순서 의존 | 격리 |
| 6 | 실시간 대기로 타임아웃 시험 | 느림·불안정 | 시간 주입 |
| 7 | 정상 데이터만 시험 | 실전 실패 | 비정상 케이스 |
| 8 | 모의가 실제와 괴리 | 거짓 안심 | bag 활용 |
| 9 | 각도·수치 경계 미시험 | 대표적 버그 | 경계값 시험 |
| 10 | 부동소수 정확 비교 | 간헐 실패 | 허용 오차 |
| 11 | 난수 시드 미고정 | 불안정 | 고정 |
| 12 | 불안정 시험 방치 | 신뢰 상실 | 즉시 대응 |
| 13 | 상위 층에서만 시험 | 느림·원인 불명 | 피라미드 |
| 14 | QoS 시험 누락 | 현장 미연결 | 인터페이스 시험 |
| 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로 시스템 전체를 검증하는 방법을 다룬다.