TrendShift 딥다이브 · 2026-07-13 · Daily #15

catchorg/Catch2 딥다이브
— C++를 위한 현대적 단위 테스트 프레임워크

Catch2C++ 코드가 올바르게 동작하는지 자동으로 검사하는 "단위 테스트 프레임워크"다. 가장 큰 특징은 테스트를 쓰는 문법이 놀랄 만큼 자연스럽다는 것 — 보통의 프레임워크는 EXPECT_EQ(a, b)처럼 비교 종류마다 다른 매크로를 외워야 하지만, Catch2는 그냥 REQUIRE(a == b)라고 평범한 C++ 표현식을 쓰면, 실패했을 때 양쪽 값을 알아서 뜯어 보여준다(표현식 분해). 여기에 BDD 문법·마이크로 벤치마크·매트처·데이터 주도 테스트(제너레이터)까지 한 몸에 들어 있다. v2까지는 헤더 파일 하나(single-header)였지만, v3에서 컴파일되는 정적 라이브러리로 바꿔 빌드 시간을 크게 줄였다. (저장소: catchorg/Catch2 · Boost Software License 1.0 · C++14 이상 · v3.15.2 · src 289파일 / 29,470줄 · 최신 커밋 2026-07-08 · TrendShift 오늘 Daily #15)
목차
  1. 프로젝트 한 줄 요약
  2. 왜 지금 주목받는가
  3. 기술 스택 전체 지도
  4. 아키텍처 심화 분석
  5. 디렉토리 구조 해부
  6. 학습 포인트
  7. 시스템 · 빌드 요구사항
  8. 직접 해볼 수 있는 실습 과제
  9. 관련 기술 심화 학습 로드맵
  10. 핵심 키워드 사전
  11. 참고 링크

1프로젝트 한 줄 요약

이름부터 뜯어보기 — "Catch" = C++ Automated Test Cases in Headers, "2" = 2세대(v2·v3)

Catch2"C++ 코드에게 시험지를 내고 자동 채점하는 도구"다. 내가 만든 함수 Factorial(3)이 정말 6을 돌려주는지, 조건을 바꿔도 안 깨지는지를 사람이 매번 눈으로 확인하는 대신, 테스트 코드로 한 번 적어두면 빌드할 때마다 기계가 대신 확인해 준다. 이런 도구를 단위 테스트 프레임워크(unit test framework)라 부른다.

이름의 유래가 성격을 잘 말해준다. 원조 "Catch"는 C++ Automated Test Cases in Headers의 머리글자로, "헤더 파일 하나만 include하면 바로 쓰는 테스트 도구"라는 뜻이었다. Catch2는 그 2세대이며, 현재 배포되는 v3는 "헤더 하나" 철학을 일부 내려놓고 컴파일 라이브러리로 재설계했다(왜 그랬는지는 §2·§4에서 자세히).

한 문장 비유

"정답만 O/X로 알려주는 채점기"가 아니라, "왜 틀렸는지 네 답과 정답을 나란히 펼쳐 보여주는 첨삭 선생님"

대부분의 테스트 도구는 ab가 같은지 물으면 "틀림"이라는 결과만 준다. 무엇이 어떻게 틀렸는지 알려면 EXPECT_EQ·EXPECT_LT처럼 비교마다 전용 매크로를 골라 써야 한다.

Catch2는 다르다. REQUIRE(Factorial(0) == 1)이라고 평범하게 쓰면, 실패했을 때 "0 == 1이라서 실패"라고 양쪽 실제 값을 저절로 펼쳐준다. 마치 시험지에 "네 답: 0 / 정답: 1"이라 첨삭해 주는 선생님처럼. 이 마법의 정체가 바로 표현식 분해(expression decomposition)이고, Catch2가 사랑받는 첫 번째 이유다.

핵심 용어
표현식 분해 (Expression Decomposition)
REQUIRE(a == b)라는 하나의 표현식을 프레임워크가 a, 비교 연산자, b쪼개어 각각의 값을 붙잡는 기술. 덕분에 사용자는 비교 종류마다 매크로를 외울 필요가 없고, 실패 메시지에 양쪽 실제 값이 자동으로 찍힌다. 이걸 C++에서 구현하는 연산자 우선순위 트릭이 Catch2 내부의 백미다(§4-1).
핵심 용어
TEST_CASE / SECTION
Catch2에서 테스트를 담는 두 그릇. TEST_CASE("이름", "[태그]")테스트 함수 하나(이름이 유효한 식별자가 아니어도 됨 — 그냥 문장으로 씀), SECTION은 그 안에서 공통 준비 코드를 공유하면서 갈래를 나누는 블록이다. 핵심 마법: SECTION 하나하나가 TEST_CASE를 처음부터 다시 실행시킨다 — 그래서 별도 fixture 클래스 없이도 매번 깨끗한 상태로 테스트할 수 있다(§4-2).

2왜 지금 주목받는가

TrendShift Daily #15 · "자연스러운 문법 + 올인원 + v3의 컴파일 타임 혁신"이라는 3박자

Catch2는 갓 나온 신생 프로젝트가 아니라, C++ 세계에서 오래 쓰여온 대표 테스트 프레임워크 중 하나다. 그런 성숙한 저장소가 트렌딩에 다시 오른다는 건, 최근 릴리스·C++ 생태계의 관심이 이 프로젝트로 몰리고 있다는 신호다. (TrendShift의 굵은 숫자는 별 수가 아니라 사회적 언급 모멘텀이다.) 사랑받는 이유를 네 갈래로 정리한다.

주목 포인트 1 — "외울 매크로가 없다" (표현식 분해)

GoogleTest는 EXPECT_EQ, EXPECT_NE, EXPECT_LT, EXPECT_GT… 비교마다 다른 매크로를 쓴다. Catch2는 전부 REQUIRE(...) 하나로 끝난다. REQUIRE(a == b), REQUIRE(x < y), REQUIRE(v.size() >= 3) — 그냥 참/거짓이 되는 평범한 C++ 표현식을 넣으면, 실패 시 양쪽 값을 알아서 출력한다. 배울 게 적고, 코드가 자연어처럼 읽힌다.

주목 포인트 2 — fixture 없는 SECTION 트리 모델

보통 프레임워크에서 "공통 준비 코드"를 공유하려면 SetUp()/TearDown()이 있는 fixture 클래스를 만들어야 한다. Catch2는 SECTION마다 TEST_CASE 본문을 통째로 다시 실행하는 방식이라, 준비 코드를 그냥 함수 맨 위에 지역 변수로 두면 각 SECTION이 매번 새 복사본을 받는다. 클래스·상속 없이 setup/teardown이 해결되는 독특하고 직관적인 설계다.

주목 포인트 3 — 테스트에 필요한 게 다 들어 있다 (올인원)

단순 assertion만이 아니다. BDD 문법(SCENARIO/GIVEN/WHEN/THEN), 마이크로 벤치마크(BENCHMARK), 매트처(Matchers), 데이터 주도 테스트(GENERATE)가 전부 기본 내장이다. 성능 회귀를 재려고 별도 벤치마크 라이브러리를 붙이거나, 파라미터라이즈드 테스트를 위해 부가 도구를 찾을 필요가 없다. "하나로 다 되는" 완결성이 강점이다.

주목 포인트 4 — v3의 컴파일 타임 혁신

v2 시절 Catch2는 "헤더 하나"라 편했지만, 테스트 파일마다 프레임워크 전체를 매번 재컴파일해 빌드가 느려지는 약점이 있었다. v3는 프레임워크를 정적 라이브러리로 한 번만 컴파일하고 사용자 코드는 가벼운 선언 헤더만 include하도록 바꿔, 공식 문서 기준 포함(inclusion) 오버헤드를 약 80% 줄였다. "느려서 doctest로 갈아탄다"던 사람들을 붙잡은 결정적 개선이다(§4-6).

경쟁 프레임워크 비교

프레임워크성격 · Catch2와의 차이
GoogleTest
(구글)
업계 표준. 방대하고 안정적이지만 EXPECT_* 매크로 쌍이 많고 모킹은 별도 gmock 필요. Catch2는 표현식 분해로 매크로가 단순하고, 매트처·벤치마크·BDD가 한 몸.
doctestCatch에서 영감받아 "컴파일 속도 최소화"에 극단 특화한 단일 헤더. 프로덕션 코드에 테스트를 함께 넣어도 될 만큼 가볍다. Catch2 v3는 속도를 따라가면서도 기능 풍부함(제너레이터·벤치마크·매트처)으로 차별화.
Boost.TestBoost 생태계의 테스트 도구. 강력하지만 Boost 의존에 무겁다. Catch2는 외부 의존성 0(표준 라이브러리만).

3기술 스택 전체 지도

"순수 C++ 표준 라이브러리 + 헤더/구현 분리 + 다양한 빌드 시스템 지원" — 의존성 없는 자기완결 프레임워크

Catch2의 스택은 의외로 단출하다. 외부 런타임 의존성이 하나도 없다 — 오직 C++ 표준 라이브러리만 쓴다. 대신 진짜 복잡함은 "이 라이브러리를 어떻게 컴파일하고, 사용자 프로젝트에 어떻게 물리느냐"라는 빌드·통합 쪽에 있다. 세 계층으로 나눠 본다.

계층 1 — 언어 · 표준 (프레임워크의 토대)

항목내용
C++ 표준C++14 최소(target_compile_features(Catch2 PUBLIC cxx_std_14)). C++17/20/23도 지원하며, C++20의 3방향 비교(<=>) 대응 코드까지 내부에 있다. v2.x 브랜치는 C++11 최소라 구형 툴체인용 유지보수판으로 남아 있다.
표준 라이브러리만외부 의존성 0. <vector>·<string>·<chrono> 등 표준만으로 문자열화·시간 측정·난수까지 자체 구현. "설치가 곧 링크"라 도입 장벽이 낮다.
SPDX 라이선스Boost Software License 1.0(BSL-1.0). 모든 소스 파일 헤더에 SPDX-License-Identifier: BSL-1.0이 박혀 있다. 상업적 사용·재배포에 매우 관대한 허용적 라이선스.

계층 2 — 라이브러리 내부 구성 (헤더/구현 분리)

v3의 핵심 스택 전략은 "인터페이스(선언)와 구현(정의)을 물리적으로 분리"하는 것이다. 사용자는 가벼운 선언 헤더만 include하고, 무거운 구현은 미리 컴파일된 .a/.lib에 담긴다.

src/catch2/ (289파일)라이브러리 본체. 이 중 139개가 internal/에 숨어 있다 — 구현 세부(디컴포저·assertion 처리·CLI 파서·난수 등)를 사용자 눈에서 격리.
선언 헤더catch_test_macros.hpp(TEST_CASE/SECTION/REQUIRE 등) 같은 가벼운 헤더. 사용자 테스트 TU는 이것만 include → 컴파일 빠름.
컴파일 결과물정적 라이브러리 2개 타깃 — Catch2::Catch2(코어)와 Catch2::Catch2WithMain(main() 포함). 후자를 링크하면 테스트 실행 진입점을 프레임워크가 제공.

계층 3 — 빌드 시스템 · 패키지 매니저 (소비 경로)

C++엔 표준 패키지 매니저가 없어서, Catch2는 거의 모든 빌드 도구에 대응한다.

CMake1급 시민. FetchContent(소스 자동 다운로드)·find_package(Catch2 3)(설치본) 모두 지원. 부가 헬퍼 catch_discover_tests로 개별 테스트를 CTest에 자동 등록.
Mesonmeson.build·meson_options.txt 제공. Meson 프로젝트에서 서브프로젝트로 편입.
BazelBUILD.bazel·MODULE.bazel(Bzlmod) 제공.
Conan · vcpkgconanfile.py 포함. vcpkg·Conan 중앙 레지스트리에도 등록돼 vcpkg install catch2 한 줄로 설치 가능.
amalgamated (단일 TU)빌드 시스템이 아예 없을 때: extras/catch_amalgamated.hpp(14,657줄) + extras/catch_amalgamated.cpp(12,403줄) 두 파일만 프로젝트에 떨어뜨리면 끝. 단 컴파일 타임은 손해라 "주력 방식은 아니다"라고 공식 명시.
비유 — 조립식 가구의 두 배송 방식

v2의 단일 헤더는 "완제품 통짜 배송"이었다 — 받자마자 쓰지만, 방(테스트 파일)마다 똑같은 가구를 매번 새로 조립(컴파일)해야 해서 느렸다. v3의 정적 라이브러리는 "공장에서 한 번 조립해 창고에 둔 뒤, 각 방은 '가져다 놓기(링크)'만" 하는 방식이다. 그래서 방이 100개라도 조립은 한 번뿐 — 이게 컴파일 타임 80% 절감의 정체다. amalgamated는 "그래도 통짜가 편한 사람"을 위한 호환 옵션.

4아키텍처 심화 분석

Catch2의 4대 마법 — 표현식 분해 · 자기등록 · SECTION 트리 실행 · 이벤트 기반 리포터

Catch2는 겉보기엔 매크로 몇 개지만, 그 안에는 C++ 템플릿·매크로·연산자 우선순위를 총동원한 정교한 장치가 숨어 있다. 이 절이 이 문서의 핵심 — 네 개의 메커니즘을 하나씩 해부한다.

4-1. 표현식 분해 — 연산자 우선순위 해킹 (catch_decomposer.hpp)

REQUIRE(a == b)는 어떻게 ab의 값을 둘 다 붙잡을까? 비밀은 매크로가 표현식 앞에 Catch::Decomposer{} <=를 몰래 끼워넣는 데 있다.

사용자가 쓴 것: REQUIRE( a == b ) │ 매크로가 바꾼 것: Catch::Decomposer{} <= a == b │ C++ 우선순위 규칙 적용 ( <= 는 == 보다 우선순위가 "낮다" ) │ ┌─────────────────┴─────────────────┐ 1단계: (Decomposer{} <= a) 가 먼저 평가 → a 를 ExprLhs<T> 로 포장 │ 2단계: ExprLhs<T>.operator==(b) 호출 → BinaryExpr(a, "==", b) 생성 │ 양쪽 값 a, b 를 모두 저장 → 실패 시 "0 == 1" 출력

실제 코드는 이렇게 생겼다(핵심만 발췌):

struct Decomposer {
    template <typename T>
    constexpr friend auto operator <= ( Decomposer&&, T&& lhs ) -> ExprLhs<T const&> {
        return ExprLhs<const T&>{ lhs };   // LHS(a)를 먼저 붙잡는다
    }
};
// ExprLhs 안에는 ==, !=, <, >, <=, >= 연산자가 정의돼
// 각각 BinaryExpr<LhsT, RhsT>(양쪽 값 저장)를 만든다

포인트: <===보다 우선순위가 낮기 때문에 Decomposer{} <= a가 먼저 묶여 a가 포장되고, 그다음 == b가 처리된다. 사용자 타입에 비교 연산자 오버로드 보일러플레이트를 강요하지 않고도 양쪽 값을 스트림 출력할 수 있는 것이 이 트릭 덕분이다. 단, &&·||는 단락 평가 때문에 값을 못 붙잡으므로 assertion 안에서 static_assert로 금지된다(대신 매트처를 쓰라고 안내).

핵심 용어
ExprLhs / BinaryExpr
ExprLhs<T>는 "비교식의 왼쪽 값을 포장한 임시 객체"로, 내부에 ==·< 같은 연산자를 갖고 있다. 이 연산자가 호출되면 왼쪽·오른쪽 값과 연산자 기호를 모두 담은 BinaryExpr를 만든다. 결국 a == b라는 한 줄이 "값·기호·값"의 구조화된 데이터로 바뀌어, 실패 메시지에 그대로 펼쳐진다.

4-2. SECTION 트리 실행 모델

Catch2에서 가장 독특한 부분. 아래 예제(examples/100-Fix-Section.cpp)를 보자.

TEST_CASE( "vectors can be sized and resized", "[vector]" ) {
    std::vector<int> v( 5 );          // (A) 준비 코드 — SECTION마다 새로 실행됨
    REQUIRE( v.size() == 5 );

    SECTION( "resizing bigger" ) {
        v.resize( 10 );
        REQUIRE( v.size() == 10 );
    }
    SECTION( "resizing smaller" ) {
        v.resize( 0 );
        REQUIRE( v.size() == 0 );
    }
}

이 TEST_CASE는 두 번 실행된다. 첫 실행 때는 첫 SECTION만 "열리고" 둘째는 건너뛴다. 둘째 실행 때는 반대. 매 실행마다 (A)의 std::vector v(5)새로 만들어지므로, 한 SECTION에서 벡터를 망가뜨려도 다른 SECTION은 깨끗한 벡터로 시작한다. SECTION을 중첩하면 트리가 되고, Catch2는 매 실행에 리프(leaf) 하나씩 경로를 밟으며 모든 조합을 순회한다.

TEST_CASE 본문 │ (실행 1) │ (실행 2) v(5) 새로 생성 v(5) 새로 생성 │ │ ┌──────┴──────┐ ┌──────┴──────┐ [열림] [건너뜀] [건너뜀] [열림] "bigger" "smaller" "bigger" "smaller" v→10 ✓ v→0 ✓

4-3. 테스트 자기등록 — 정적 초기화 트릭 (catch_test_registry.hpp)

TEST_CASE(...)를 쓰기만 하면 main()에 아무것도 안 적었는데 테스트가 실행된다. 어떻게? 매크로가 익명 네임스페이스에 전역 객체 하나를 심고, 그 생성자가 프로그램 시작 전(static init)에 테스트를 레지스트리에 등록하기 때문이다.

// TEST_CASE( "name", "[tag]" ) 는 대략 이렇게 펼쳐진다:
static void autoName();                    // (1) 사용자 테스트 함수 선언
namespace {
  const Catch::AutoReg autoRegistrar(       // (2) 전역 객체 — 생성자가 등록 실행
      Catch::makeTestInvoker( &autoName ),
      CATCH_INTERNAL_LINEINFO,
      Catch::StringRef(),
      Catch::NameAndTags{ "name", "[tag]" } );
}
static void autoName()                     // (3) 여기부터 사용자가 쓴 { ... } 본문

AutoReg 생성자가 함수 포인터를 전역 테스트 레지스트리에 밀어넣는다. 이름 충돌은 INTERNAL_CATCH_UNIQUE_NAME(__COUNTER__ 기반)으로 방지. 이 "전역 객체 생성자로 자기등록" 관용구는 플러그인·팩토리 자동 등록에 두루 쓰이는 C++ 패턴의 교과서 사례다.

4-4. 이벤트 기반 리포터 아키텍처 (interfaces/catch_interfaces_reporter.hpp)

테스트 결과를 콘솔·XML·JUnit 등 다양한 형식으로 뽑는 부분은 이벤트 리스너 패턴으로 설계됐다. 테스트가 진행되며 발생하는 사건마다 콜백이 불린다.

class IEventListener {
    virtual void testRunStarting( TestRunInfo const& ) = 0;
    virtual void testCaseStarting( TestCaseInfo const& ) = 0;
    virtual void sectionStarting( SectionInfo const& ) = 0;
    virtual void assertionEnded( AssertionStats const& ) = 0;   // REQUIRE 결과 하나
    virtual void testCaseEnded( TestCaseStats const& ) = 0;
    virtual void testRunEnded( TestRunStats const& ) = 0;
    // ... benchmarkStarting, listReporters 등
};

구현체는 두 부류로 갈린다 — streaming_base(결과가 나오는 대로 즉시 출력, 콘솔용)와 cumulative_base(전부 모았다가 마지막에 한꺼번에 출력, XML/JUnit처럼 문서 구조가 필요한 경우). 여러 리스너를 동시에 물릴 땐 multi 리포터가 팬아웃(fan-out)한다. 제공되는 출력 리포터는 9종이다.

리포터용도
console (기본)사람이 읽는 컬러 터미널 출력.
compact한 줄에 한 결과, 최소 출력.
xmlCatch2 고유 XML(도구 연동용).
junitJUnit XML — Jenkins 등 CI가 바로 읽음.
sonarqubeSonarQube 정적분석 플랫폼용.
tapTest Anything Protocol.
teamcityJetBrains TeamCity CI용.
automakeGNU Automake .trs 형식.
json기계 파싱용 JSON.

4-5. 매트처 · 제너레이터 · 벤치마크

assertion 위에 얹히는 세 확장 기능. 각각 실제 문법을 보자.

매트처(Matchers) — REQUIRE_THAT

using Catch::Matchers::EndsWith;
using Catch::Matchers::ContainsSubstring;

REQUIRE_THAT( getSomeString(),
              EndsWith("as a service") && ContainsSubstring("web scale") );  // 매트처 합성
CHECK_THAT( value, WithinAbs(0, 2e-2) && !WithinULP(0., 1) );          // 부동소수점 비교

문자열(StartsWith·EndsWith·ContainsSubstring·Matches 정규식), 부동소수점(WithinAbs·WithinRel·WithinULP), 컨테이너(SizeIs·IsEmpty·UnorderedEquals·AllMatch) 등이 있고, &&·||·!합성된다. 부동소수점 비교처럼 ==로 표현 못 하는 검증을 자연스럽게 처리한다.

제너레이터(Generators) — GENERATE로 데이터 주도

TEST_CASE("Table 입력/출력", "[generator]") {
    auto [input, expected] = GENERATE( table<std::string, size_t>({
        { "one", 3 }, { "two", 3 }, { "three", 5 }, { "four", 4 } }) );
    REQUIRE( getLength(input) == expected );   // 4개 값마다 자동 재실행
}

GENERATE가 있는 SECTION은 생성된 값 개수만큼 자동 반복된다(SECTION 재실행 모델의 응용). table<>·range()·random()·values()와 커스텀 제너레이터로 파라미터라이즈드 테스트를 표현.

벤치마크(Benchmark) — BENCHMARK

TEST_CASE("Fibonacci 성능") {
    CHECK( Fibonacci(5) == 8 );
    BENCHMARK("Fibonacci 20") { return Fibonacci(20); };   // 세미콜론 필수(람다)
    BENCHMARK("Fibonacci 35") { return Fibonacci(35); };
}

BENCHMARK는 람다로 확장돼 자동 반복 측정 + 평균·표준편차·이상치(outlier) 분석까지 해준다. return을 쓰는 이유는 컴파일러가 "결과를 안 쓰네?" 하고 코드를 최적화로 지워버리는 것을 막기 위함(benchmark/catch_optimizer.hpp). 별도 벤치마크 라이브러리 없이 테스트와 성능 측정을 한 파일에서 한다.

4-6. v3 컴파일 타임 방화벽(firewall) 설계

왜 v3가 그렇게 빠른가를 그림으로 정리한다.

[ v2 · 단일 헤더 ] [ v3 · 정적 라이브러리 ] test_a.cpp ─┐ test_a.cpp ─┐ (가벼운 선언 헤더만) test_b.cpp ─┤ 각자 프레임워크 test_b.cpp ─┤ test_c.cpp ─┤ "전체"를 매번 컴파일 test_c.cpp ─┘ ▼ (느림, O(파일수)) │ link 프레임워크×3 재컴파일 libCatch2.a (딱 1번 컴파일) ▼ 포함 오버헤드 약 80% ↓

핵심은 interfaces/(추상 인터페이스)와 internal/(구현 139파일)의 물리적 분리다. 구현이 바뀌어도 사용자 TU가 include하는 건 인터페이스뿐이라 재빌드가 전파되지 않는다. 게다가 매트처·제너레이터·벤치마크는 쓰지 않으면 해당 헤더를 include하지 않으므로 컴파일 비용이 아예 발생하지 않는다. "안 쓰면 공짜"인 모듈러 설계가 대규모 C++ 라이브러리 아키텍처의 모범으로 꼽히는 이유다.

5디렉토리 구조 해부

본체는 src/catch2/ 하나. 그 안에서 internal/이 절반(139파일)을 차지 — "구현은 숨기고 인터페이스만 노출"

Catch2의 저장소는 라이브러리 소스(src/) + 문서(docs/) + 예제(examples/) + 소비자용 헬퍼(extras/)로 나뉜다. 프레임워크 코드의 뼈대인 src/catch2/부터 본다.

src/catch2/ ├── catch_test_macros.hpp ★ TEST_CASE / SECTION / REQUIRE 등 사용자용 진입 헤더 ├── catch_version_macros.hpp 버전 매크로 (MAJOR 3 / MINOR 15 / PATCH 2) ├── catch_session.hpp 테스트 세션 실행 진입점 │ ├── internal/ (139파일) ★ 구현 은닉처: decomposer·assertion 처리·clara(CLI 파서) │ ·context·RNG(난수)·문자열화·전처리기 매크로 … ├── matchers/ 매트처: string·floating_point(WithinRel/Abs/ULP)·vector │ ·quantifiers(AllMatch)·container_properties(SizeIs)·exception … ├── generators/ 데이터 주도: generators·adapters·random·range·table ├── benchmark/ 마이크로 벤치마크: chronometer·clock·estimate·optimizer │ ·outlier_classification·sample_analysis + detail/ ├── interfaces/ 추상 인터페이스(ABI 경계): IEventListener·config·exception │ ·registry_hub·testcase·test_invoker … └── reporters/ 출력 리포터 9종(console·xml·junit·sonarqube·tap·teamcity …)

최상위 디렉토리

디렉토리 / 파일역할
src/라이브러리 소스 289파일 / 29,470줄. 위 catch2/가 그 내용.
docs/ (35개 .md)문서 — tutorial·assertions·matchers·generators·benchmarks·cmake-integration·migrate-v2-to-v3·command-line 등. 공식 문서가 곧 저장소 안에 있다.
examples/ (16개)010-TestCase.cpp부터 311-Gen-CustomCapture.cpp까지 번호순 예제. §4의 코드가 다 여기서 나왔다 — 입문자는 이 폴더가 최고의 교재.
extras/소비자용 CMake 헬퍼 + amalgamated. Catch.cmake(catch_discover_tests)·CatchAddTests.cmake·CatchShardTests.cmake(테스트 샤딩)·catch_amalgamated.hpp/.cpp·gdbinit·lldbinit(디버거 설정).
tests/Catch2가 자기 자신을 테스트하는 코드(SelfTest·단위·통합·승인 테스트). "테스트 프레임워크를 어떻게 테스트하나"의 실물.
CMake/ · tools/ · fuzzing/빌드 스크립트 모듈 / 릴리스·소스 생성 스크립트 / 퍼징 하니스.
빌드 파일들CMakeLists.txt·meson.build·BUILD.bazel·MODULE.bazel·conanfile.py·CMakePresets.json — 다중 빌드 시스템 대응(§3).
핵심 관찰
internal/이 139파일 = 전체 절반
사용자가 실제로 include하는 헤더는 몇 개 안 되지만, 구현 파일이 전체의 절반을 차지한다. 이는 §4-6의 "컴파일 타임 방화벽" 설계가 디렉토리 구조에 그대로 반영된 것 — 복잡한 세부는 전부 internal/에 가두고, 사용자에겐 얇은 인터페이스만 보여준다. 잘 설계된 라이브러리의 전형적 모습이다.

6학습 포인트

이 저장소에서 진짜로 배울 것 — "매크로+템플릿 협업 + 확장 가능한 라이브러리 설계"

Catch2는 "테스트 쓰는 법"을 넘어, 현대 C++의 고급 기법이 어떻게 실전에서 쓰이는지를 보여주는 살아있는 교재다. 기술별로 배울 거리를 정리한다.

배울 것 1 — 연산자 우선순위를 이용한 표현식 캡처

§4-1의 Decomposer{} <= a == b언어의 우선순위 규칙 자체를 도구로 쓰는 발상의 전환이다. 여기에 SFINAE(enable_if)로 값 캡처/참조 캡처를 분기하는 기법까지 얹혀 있다. "매크로로 표현식을 감싸고, 템플릿으로 그 표현식을 분해한다"는 매크로+템플릿 협업의 정수를 볼 수 있다.

배울 것 2 — 정적 초기화 기반 자기등록 패턴

§4-3의 AutoReg 관용구는 테스트뿐 아니라 플러그인·팩토리·직렬화 타입 등록에 두루 쓰인다. "익명 네임스페이스에 전역 객체를 만들고, 그 생성자가 main 이전에 중앙 레지스트리에 자기를 등록한다"는 패턴은 C++ 개발자라면 반드시 알아둘 도구다. __COUNTER__로 고유 심볼을 만드는 매크로 기법도 함께.

배울 것 3 — 컴파일 타임을 고려한 아키텍처

C++에서 컴파일 시간은 곧 개발 생산성이다. Catch2 v3가 인터페이스/구현을 분리(interfaces/ vs internal/)하고 기능별로 헤더를 쪼개 "안 쓰면 비용 0"을 달성한 방식은, 대규모 헤더 라이브러리를 설계할 때 그대로 참고할 만한 실전 아키텍처 결정이다.

배울 것 4 — 이벤트 기반 확장 설계

§4-4의 IEventListener 순수 가상 인터페이스 + 다형 리포터 + 멀티플렉서 구조는, 출력 형식(9종)과 커스텀 관찰자를 코어 수정 없이 플러그인화하는 방법을 보여준다. "확장 지점을 인터페이스로 열어두는" 개방-폐쇄 원칙(OCP)의 구체적 사례.

배울 것 5 — 매크로로 자연스러운 DSL 만들기

TEST_CASE·SECTION·GENERATE·BENCHMARK·SCENARIO는 전부 매크로지만, 트리 재실행·데이터 주도·람다 벤치마크 같은 고급 시맨틱을 자연어 같은 문법으로 노출한다. "매크로는 나쁘다"는 통념을 넘어, 언제 어떻게 매크로가 최선의 인터페이스가 되는지를 배운다.

실습 아이디어 (가볍게)
미니 표현식 분해기 30줄로 흉내내기

DecomposerExprLhs를 아주 단순화해, operator<=로 왼쪽 값을 잡고 operator==로 오른쪽 값을 잡아 "3 == 5"처럼 출력하는 30줄짜리 미니 버전을 직접 짜보면, §4-1의 마법이 "마법이 아니라 우선순위 규칙"이었음을 몸으로 이해하게 된다.

7시스템 · 빌드 요구사항

"컴파일러 하나면 끝" — 무거운 사전 준비가 없는 게 강점

Catch2는 실행 환경 요구가 매우 가볍다. C++14를 지원하는 컴파일러 하나면 되고, 외부 라이브러리 설치가 필요 없다.

항목요구
C++ 표준v3 = C++14 최소(C++17/20/23 지원). C++11만 되는 구형 환경은 v2.x 브랜치를 쓴다.
컴파일러GCC · Clang · AppleClang · MSVC(cl /EHsc). Linux·macOS·Windows CI에서 상시 검증. Android 로그 출력 지원도 내장.
외부 의존성없음(표준 라이브러리만). 문자열화·시간 측정·난수까지 자체 구현.
빌드 도구CMake 3.x 권장(FetchContent/find_package). Meson·Bazel·Conan·vcpkg도 지원(§3).

실전 통합 — CMake FetchContent (가장 흔한 방법)

Include(FetchContent)
FetchContent_Declare(
  Catch2
  GIT_REPOSITORY https://github.com/catchorg/Catch2.git
  GIT_TAG        v3.15.2 )
FetchContent_MakeAvailable(Catch2)

target_link_libraries(tests PRIVATE Catch2::Catch2WithMain)   # main() 포함본을 링크

설치된 Catch2를 쓸 땐 find_package(Catch2 3 REQUIRED)로 바꾸면 된다. 커스텀 main()을 직접 쓰고 싶으면 Catch2::Catch2WithMain 대신 Catch2::Catch2(코어만)를 링크한다.

CTest 자동 등록 — catch_discover_tests

find_package(Catch2 REQUIRED)
include(Catch)                        # extras/Catch.cmake
target_link_libraries(tests PRIVATE Catch2::Catch2)
catch_discover_tests(tests)           # 바이너리를 실행해 개별 TEST_CASE를 CTest에 자동 등록

catch_discover_tests는 컴파일된 테스트 실행 파일을 한 번 돌려 그 안의 모든 TEST_CASE를 개별 CTest 테스트로 등록한다. 덕분에 ctest 한 줄로 병렬 실행·필터·재실행이 된다. 대규모 테스트는 CatchShardTests.cmake로 여러 머신에 쪼개(shard) 돌릴 수도 있다.

실행 옵션 — 재현 가능한 랜덤 순서
--order rand · --rng-seed
Catch2 3.9.0부터 테스트 실행 순서 랜덤화가 기본값이다("A 테스트가 B의 뒷정리에 몰래 의존"하는 숨은 결합을 드러내려는 것). --rng-seed로 시드를 고정하면 언제나 같은 순서로 재현되고, 태그로 일부만 골라 돌려도 상대 순서가 불변(subset-invariant)이라 실패를 안정적으로 재현할 수 있다. 리포터가 사용한 시드를 출력에 기록해 준다.

8직접 해볼 수 있는 실습 과제

읽기만 하면 안 남는다 — 난이도별로 손을 움직여 Catch2를 체득하는 과제 5개
난이도 ★☆☆ · 입문

1. 첫 TEST_CASE와 표현식 분해 눈으로 보기

CMake FetchContent로 Catch2를 붙이고, Factorial 함수를 만들어 일부러 틀린 기대값으로 REQUIRE(Factorial(0) == 2)를 써본다. 빌드·실행하면 "1 == 2"라고 양쪽 값이 찍히는 표현식 분해를 직접 목격한다. §4-1이 손에 잡히는 순간.

난이도 ★☆☆ · 입문

2. SECTION으로 fixture 없이 상태 공유하기

std::vector를 준비 코드로 두고 SECTION 두 개에서 각각 push_back·clear를 해본다. 한 SECTION에서 벡터를 비워도 다른 SECTION은 원래 벡터로 시작하는 걸 확인해, "SECTION마다 TEST_CASE가 재실행된다"는 §4-2 모델을 체감한다.

난이도 ★★☆ · 중급

3. GENERATE로 데이터 주도 테스트 만들기

회문(palindrome) 판별 함수를 만들고, GENERATE(table<...>({...}))여러 입력·기대값 쌍을 한 번에 검증한다. 각 데이터마다 테스트가 자동 반복되고, 실패한 케이스만 콕 집어 보고되는 걸 확인 — 파라미터라이즈드 테스트의 편함을 배운다.

난이도 ★★☆ · 중급

4. 매트처와 BDD 문법 써보기

문자열 유틸을 REQUIRE_THAT(s, StartsWith("Hello") && ContainsSubstring("world"))로 검증하고, 같은 로직을 SCENARIO/GIVEN/WHEN/THEN BDD 문법으로도 다시 써본다. 같은 테스트가 두 가지 표현으로 자연어처럼 읽히는 경험을 한다.

난이도 ★★★ · 심화

5. BENCHMARK + CTest 통합 + 랜덤 시드 실험

정렬 알고리즘 두 개를 BENCHMARK로 재서 평균·표준편차를 비교하고, catch_discover_tests로 CTest에 등록한 뒤 --order rand --rng-seed 42순서를 고정 재현해 본다. 성능 측정·CI 통합·재현성이라는 실무 3종을 한 번에 다룬다.

9관련 기술 심화 학습 로드맵

"C++ 중급자 → Catch2 능숙 사용자·기여자"까지, 4주 코스
주차목표 · 학습 내용
1주차
기본기
TEST_CASE/SECTION 모델, REQUIRE vs CHECK, 태그([tag])로 필터링, examples/ 폴더 통독. → 과제 1·2로 표현식 분해·SECTION 재실행 체득.
2주차
확장 기능
매트처(문자열·부동소수점·컨테이너), GENERATE 데이터 주도, BDD 문법. → 과제 3·4로 "제대로 된 테스트 스위트" 작성.
3주차
빌드·CI 통합
CMake FetchContent/find_package, catch_discover_tests, JUnit/SonarQube 리포터로 CI 연동, 테스트 샤딩, 랜덤 순서·시드 재현. → 과제 5.
4주차
내부·기여
표현식 분해(catch_decomposer.hpp)·자기등록(catch_test_registry.hpp)·리포터 인터페이스 코드 읽기, 커스텀 매트처/리포터 작성, tests/의 자기테스트 구조 파악. → 실제 이슈·PR 도전.
학습 순서 팁

Catch2는 "쓰기는 5분, 깊이 알기는 오래"인 도구다. 처음엔 REQUIRE·SECTION만으로 충분하니 일단 내 프로젝트에 테스트를 붙여 쓰면서, 매트처→제너레이터→벤치마크로 필요할 때 하나씩 넓히는 게 정석이다. 내부 구현(§4)은 "왜 이렇게 자연스럽지?"가 궁금해질 때 examples/docs/를 왕복하며 파면 된다.

10핵심 키워드 사전

이 문서에 나온 용어를 한 번에 — Catch2 문서·코드를 읽을 때 계속 마주칠 단어들
용어
TEST_CASE테스트 하나를 담는 매크로. 이름을 자유 문장으로 쓸 수 있고 [태그]를 붙인다.
SECTIONTEST_CASE 안의 갈래. SECTION마다 본문이 재실행돼 fixture 없이 상태를 공유.
REQUIRE / CHECK단언(assertion). REQUIRE는 실패 시 즉시 중단, CHECK는 실패해도 계속.
표현식 분해REQUIRE(a == b)를 쪼개 양쪽 값을 자동 출력하는 Catch2의 대표 기술.
Decomposer / ExprLhs표현식 분해를 구현하는 내부 객체. operator<= 우선순위 트릭으로 왼쪽 값을 먼저 포장.
REQUIRE_THAT / Matcher매트처를 쓰는 단언. StartsWith·WithinRel 등으로 ==로 표현 못 하는 검증.
GENERATE / Generator여러 입력값을 자동 반복시키는 데이터 주도 테스트 도구(table·range·random).
BENCHMARK코드 조각의 실행 시간을 반복 측정(평균·표준편차·이상치)하는 마이크로 벤치마크.
SCENARIO / GIVEN·WHEN·THENBDD 스타일 매크로. TEST_CASE/SECTION의 자연어 별칭.
Tag ([tag])테스트에 붙이는 꼬리표. [a][b]=둘 다, [a],[b]=둘 중 하나, [.]=기본 실행에서 숨김.
AutoRegTEST_CASE를 전역 레지스트리에 자동 등록하는 정적 초기화 객체.
IEventListener / Reporter테스트 진행 이벤트를 받아 출력을 만드는 인터페이스와 그 구현(console·junit 등 9종).
amalgamated빌드 시스템 없이 쓰도록 전체를 .hpp+.cpp 두 파일로 합친 배포본.
catch_discover_tests테스트 바이너리를 실행해 개별 TEST_CASE를 CTest에 자동 등록하는 CMake 헬퍼.
BSL-1.0Boost Software License 1.0. 상업 사용·재배포에 관대한 허용적 라이선스.

11참고 링크

공식 소스부터 학습 문서까지
구분링크
GitHubgithub.com/catchorg/Catch2 — 이 저장소(소스·문서·예제)
튜토리얼docs/tutorial.md — 첫 테스트부터 차근차근(입문 필독)
v2→v3 마이그레이션docs/migrate-v2-to-v3.md — 단일 헤더에서 라이브러리로 옮기는 법
CMake 통합docs/cmake-integration.md — FetchContent·find_package·catch_discover_tests
레퍼런스 문서docs/assertions·matchers·generators·benchmarks·command-line
예제 모음examples/ — 번호순 실전 예제 16개(최고의 실습 교재)
온라인 시연Godbolt Compiler Explorer에서 설치 없이 Catch2 테스트를 바로 실행 가능