현대적 단위 테스트 프레임워크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)
Catch2는 "C++ 코드에게 시험지를 내고 자동 채점하는 도구"다. 내가 만든 함수 Factorial(3)이 정말 6을 돌려주는지, 조건을 바꿔도 안 깨지는지를 사람이 매번 눈으로 확인하는 대신, 테스트 코드로 한 번 적어두면 빌드할 때마다 기계가 대신 확인해 준다. 이런 도구를 단위 테스트 프레임워크(unit test framework)라 부른다.
이름의 유래가 성격을 잘 말해준다. 원조 "Catch"는 C++ Automated Test Cases in Headers의 머리글자로, "헤더 파일 하나만 include하면 바로 쓰는 테스트 도구"라는 뜻이었다. Catch2는 그 2세대이며, 현재 배포되는 v3는 "헤더 하나" 철학을 일부 내려놓고 컴파일 라이브러리로 재설계했다(왜 그랬는지는 §2·§4에서 자세히).
대부분의 테스트 도구는 a와 b가 같은지 물으면 "틀림"이라는 결과만 준다. 무엇이 어떻게 틀렸는지 알려면 EXPECT_EQ·EXPECT_LT처럼 비교마다 전용 매크로를 골라 써야 한다.
Catch2는 다르다. REQUIRE(Factorial(0) == 1)이라고 평범하게 쓰면, 실패했을 때 "0 == 1이라서 실패"라고 양쪽 실제 값을 저절로 펼쳐준다. 마치 시험지에 "네 답: 0 / 정답: 1"이라 첨삭해 주는 선생님처럼. 이 마법의 정체가 바로 표현식 분해(expression decomposition)이고, Catch2가 사랑받는 첫 번째 이유다.
REQUIRE(a == b)라는 하나의 표현식을 프레임워크가 a, 비교 연산자, b로 쪼개어 각각의 값을 붙잡는 기술. 덕분에 사용자는 비교 종류마다 매크로를 외울 필요가 없고, 실패 메시지에 양쪽 실제 값이 자동으로 찍힌다. 이걸 C++에서 구현하는 연산자 우선순위 트릭이 Catch2 내부의 백미다(§4-1).TEST_CASE("이름", "[태그]")는 테스트 함수 하나(이름이 유효한 식별자가 아니어도 됨 — 그냥 문장으로 씀), SECTION은 그 안에서 공통 준비 코드를 공유하면서 갈래를 나누는 블록이다. 핵심 마법: SECTION 하나하나가 TEST_CASE를 처음부터 다시 실행시킨다 — 그래서 별도 fixture 클래스 없이도 매번 깨끗한 상태로 테스트할 수 있다(§4-2).Catch2는 갓 나온 신생 프로젝트가 아니라, C++ 세계에서 오래 쓰여온 대표 테스트 프레임워크 중 하나다. 그런 성숙한 저장소가 트렌딩에 다시 오른다는 건, 최근 릴리스·C++ 생태계의 관심이 이 프로젝트로 몰리고 있다는 신호다. (TrendShift의 굵은 숫자는 별 수가 아니라 사회적 언급 모멘텀이다.) 사랑받는 이유를 네 갈래로 정리한다.
GoogleTest는 EXPECT_EQ, EXPECT_NE, EXPECT_LT, EXPECT_GT… 비교마다 다른 매크로를 쓴다. Catch2는 전부 REQUIRE(...) 하나로 끝난다. REQUIRE(a == b), REQUIRE(x < y), REQUIRE(v.size() >= 3) — 그냥 참/거짓이 되는 평범한 C++ 표현식을 넣으면, 실패 시 양쪽 값을 알아서 출력한다. 배울 게 적고, 코드가 자연어처럼 읽힌다.
보통 프레임워크에서 "공통 준비 코드"를 공유하려면 SetUp()/TearDown()이 있는 fixture 클래스를 만들어야 한다. Catch2는 SECTION마다 TEST_CASE 본문을 통째로 다시 실행하는 방식이라, 준비 코드를 그냥 함수 맨 위에 지역 변수로 두면 각 SECTION이 매번 새 복사본을 받는다. 클래스·상속 없이 setup/teardown이 해결되는 독특하고 직관적인 설계다.
단순 assertion만이 아니다. BDD 문법(SCENARIO/GIVEN/WHEN/THEN), 마이크로 벤치마크(BENCHMARK), 매트처(Matchers), 데이터 주도 테스트(GENERATE)가 전부 기본 내장이다. 성능 회귀를 재려고 별도 벤치마크 라이브러리를 붙이거나, 파라미터라이즈드 테스트를 위해 부가 도구를 찾을 필요가 없다. "하나로 다 되는" 완결성이 강점이다.
v2 시절 Catch2는 "헤더 하나"라 편했지만, 테스트 파일마다 프레임워크 전체를 매번 재컴파일해 빌드가 느려지는 약점이 있었다. v3는 프레임워크를 정적 라이브러리로 한 번만 컴파일하고 사용자 코드는 가벼운 선언 헤더만 include하도록 바꿔, 공식 문서 기준 포함(inclusion) 오버헤드를 약 80% 줄였다. "느려서 doctest로 갈아탄다"던 사람들을 붙잡은 결정적 개선이다(§4-6).
| 프레임워크 | 성격 · Catch2와의 차이 |
|---|---|
| GoogleTest (구글) | 업계 표준. 방대하고 안정적이지만 EXPECT_* 매크로 쌍이 많고 모킹은 별도 gmock 필요. Catch2는 표현식 분해로 매크로가 단순하고, 매트처·벤치마크·BDD가 한 몸. |
| doctest | Catch에서 영감받아 "컴파일 속도 최소화"에 극단 특화한 단일 헤더. 프로덕션 코드에 테스트를 함께 넣어도 될 만큼 가볍다. Catch2 v3는 속도를 따라가면서도 기능 풍부함(제너레이터·벤치마크·매트처)으로 차별화. |
| Boost.Test | Boost 생태계의 테스트 도구. 강력하지만 Boost 의존에 무겁다. Catch2는 외부 의존성 0(표준 라이브러리만). |
Catch2의 스택은 의외로 단출하다. 외부 런타임 의존성이 하나도 없다 — 오직 C++ 표준 라이브러리만 쓴다. 대신 진짜 복잡함은 "이 라이브러리를 어떻게 컴파일하고, 사용자 프로젝트에 어떻게 물리느냐"라는 빌드·통합 쪽에 있다. 세 계층으로 나눠 본다.
| 항목 | 내용 |
|---|---|
| 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이 박혀 있다. 상업적 사용·재배포에 매우 관대한 허용적 라이선스. |
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() 포함). 후자를 링크하면 테스트 실행 진입점을 프레임워크가 제공. |
C++엔 표준 패키지 매니저가 없어서, Catch2는 거의 모든 빌드 도구에 대응한다.
| CMake | 1급 시민. FetchContent(소스 자동 다운로드)·find_package(Catch2 3)(설치본) 모두 지원. 부가 헬퍼 catch_discover_tests로 개별 테스트를 CTest에 자동 등록. |
| Meson | meson.build·meson_options.txt 제공. Meson 프로젝트에서 서브프로젝트로 편입. |
| Bazel | BUILD.bazel·MODULE.bazel(Bzlmod) 제공. |
| Conan · vcpkg | conanfile.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는 "그래도 통짜가 편한 사람"을 위한 호환 옵션.
Catch2는 겉보기엔 매크로 몇 개지만, 그 안에는 C++ 템플릿·매크로·연산자 우선순위를 총동원한 정교한 장치가 숨어 있다. 이 절이 이 문서의 핵심 — 네 개의 메커니즘을 하나씩 해부한다.
REQUIRE(a == b)는 어떻게 a와 b의 값을 둘 다 붙잡을까? 비밀은 매크로가 표현식 앞에 Catch::Decomposer{} <=를 몰래 끼워넣는 데 있다.
실제 코드는 이렇게 생겼다(핵심만 발췌):
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<T>는 "비교식의 왼쪽 값을 포장한 임시 객체"로, 내부에 ==·< 같은 연산자를 갖고 있다. 이 연산자가 호출되면 왼쪽·오른쪽 값과 연산자 기호를 모두 담은 BinaryExpr를 만든다. 결국 a == b라는 한 줄이 "값·기호·값"의 구조화된 데이터로 바뀌어, 실패 메시지에 그대로 펼쳐진다.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(...)를 쓰기만 하면 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++ 패턴의 교과서 사례다.
테스트 결과를 콘솔·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 | 한 줄에 한 결과, 최소 출력. |
| xml | Catch2 고유 XML(도구 연동용). |
| junit | JUnit XML — Jenkins 등 CI가 바로 읽음. |
| sonarqube | SonarQube 정적분석 플랫폼용. |
| tap | Test Anything Protocol. |
| teamcity | JetBrains TeamCity CI용. |
| automake | GNU Automake .trs 형식. |
| json | 기계 파싱용 JSON. |
assertion 위에 얹히는 세 확장 기능. 각각 실제 문법을 보자.
REQUIRE_THATusing 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) 등이 있고, &&·||·!로 합성된다. 부동소수점 비교처럼 ==로 표현 못 하는 검증을 자연스럽게 처리한다.
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()와 커스텀 제너레이터로 파라미터라이즈드 테스트를 표현.
BENCHMARKTEST_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). 별도 벤치마크 라이브러리 없이 테스트와 성능 측정을 한 파일에서 한다.
왜 v3가 그렇게 빠른가를 그림으로 정리한다.
핵심은 interfaces/(추상 인터페이스)와 internal/(구현 139파일)의 물리적 분리다. 구현이 바뀌어도 사용자 TU가 include하는 건 인터페이스뿐이라 재빌드가 전파되지 않는다. 게다가 매트처·제너레이터·벤치마크는 쓰지 않으면 해당 헤더를 include하지 않으므로 컴파일 비용이 아예 발생하지 않는다. "안 쓰면 공짜"인 모듈러 설계가 대규모 C++ 라이브러리 아키텍처의 모범으로 꼽히는 이유다.
src/catch2/ 하나. 그 안에서 internal/이 절반(139파일)을 차지 — "구현은 숨기고 인터페이스만 노출"Catch2의 저장소는 라이브러리 소스(src/) + 문서(docs/) + 예제(examples/) + 소비자용 헬퍼(extras/)로 나뉜다. 프레임워크 코드의 뼈대인 src/catch2/부터 본다.
| 디렉토리 / 파일 | 역할 |
|---|---|
| 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파일 = 전체 절반internal/에 가두고, 사용자에겐 얇은 인터페이스만 보여준다. 잘 설계된 라이브러리의 전형적 모습이다.Catch2는 "테스트 쓰는 법"을 넘어, 현대 C++의 고급 기법이 어떻게 실전에서 쓰이는지를 보여주는 살아있는 교재다. 기술별로 배울 거리를 정리한다.
§4-1의 Decomposer{} <= a == b는 언어의 우선순위 규칙 자체를 도구로 쓰는 발상의 전환이다. 여기에 SFINAE(enable_if)로 값 캡처/참조 캡처를 분기하는 기법까지 얹혀 있다. "매크로로 표현식을 감싸고, 템플릿으로 그 표현식을 분해한다"는 매크로+템플릿 협업의 정수를 볼 수 있다.
§4-3의 AutoReg 관용구는 테스트뿐 아니라 플러그인·팩토리·직렬화 타입 등록에 두루 쓰인다. "익명 네임스페이스에 전역 객체를 만들고, 그 생성자가 main 이전에 중앙 레지스트리에 자기를 등록한다"는 패턴은 C++ 개발자라면 반드시 알아둘 도구다. __COUNTER__로 고유 심볼을 만드는 매크로 기법도 함께.
C++에서 컴파일 시간은 곧 개발 생산성이다. Catch2 v3가 인터페이스/구현을 분리(interfaces/ vs internal/)하고 기능별로 헤더를 쪼개 "안 쓰면 비용 0"을 달성한 방식은, 대규모 헤더 라이브러리를 설계할 때 그대로 참고할 만한 실전 아키텍처 결정이다.
§4-4의 IEventListener 순수 가상 인터페이스 + 다형 리포터 + 멀티플렉서 구조는, 출력 형식(9종)과 커스텀 관찰자를 코어 수정 없이 플러그인화하는 방법을 보여준다. "확장 지점을 인터페이스로 열어두는" 개방-폐쇄 원칙(OCP)의 구체적 사례.
TEST_CASE·SECTION·GENERATE·BENCHMARK·SCENARIO는 전부 매크로지만, 트리 재실행·데이터 주도·람다 벤치마크 같은 고급 시맨틱을 자연어 같은 문법으로 노출한다. "매크로는 나쁘다"는 통념을 넘어, 언제 어떻게 매크로가 최선의 인터페이스가 되는지를 배운다.
Decomposer와 ExprLhs를 아주 단순화해, operator<=로 왼쪽 값을 잡고 operator==로 오른쪽 값을 잡아 "3 == 5"처럼 출력하는 30줄짜리 미니 버전을 직접 짜보면, §4-1의 마법이 "마법이 아니라 우선순위 규칙"이었음을 몸으로 이해하게 된다.
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). |
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(코어만)를 링크한다.
catch_discover_testsfind_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--rng-seed로 시드를 고정하면 언제나 같은 순서로 재현되고, 태그로 일부만 골라 돌려도 상대 순서가 불변(subset-invariant)이라 실패를 안정적으로 재현할 수 있다. 리포터가 사용한 시드를 출력에 기록해 준다.CMake FetchContent로 Catch2를 붙이고, Factorial 함수를 만들어 일부러 틀린 기대값으로 REQUIRE(Factorial(0) == 2)를 써본다. 빌드·실행하면 "1 == 2"라고 양쪽 값이 찍히는 표현식 분해를 직접 목격한다. §4-1이 손에 잡히는 순간.
std::vector를 준비 코드로 두고 SECTION 두 개에서 각각 push_back·clear를 해본다. 한 SECTION에서 벡터를 비워도 다른 SECTION은 원래 벡터로 시작하는 걸 확인해, "SECTION마다 TEST_CASE가 재실행된다"는 §4-2 모델을 체감한다.
회문(palindrome) 판별 함수를 만들고, GENERATE(table<...>({...}))로 여러 입력·기대값 쌍을 한 번에 검증한다. 각 데이터마다 테스트가 자동 반복되고, 실패한 케이스만 콕 집어 보고되는 걸 확인 — 파라미터라이즈드 테스트의 편함을 배운다.
문자열 유틸을 REQUIRE_THAT(s, StartsWith("Hello") && ContainsSubstring("world"))로 검증하고, 같은 로직을 SCENARIO/GIVEN/WHEN/THEN BDD 문법으로도 다시 써본다. 같은 테스트가 두 가지 표현으로 자연어처럼 읽히는 경험을 한다.
정렬 알고리즘 두 개를 BENCHMARK로 재서 평균·표준편차를 비교하고, catch_discover_tests로 CTest에 등록한 뒤 --order rand --rng-seed 42로 순서를 고정 재현해 본다. 성능 측정·CI 통합·재현성이라는 실무 3종을 한 번에 다룬다.
| 주차 | 목표 · 학습 내용 |
|---|---|
| 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/를 왕복하며 파면 된다.
| 용어 | 뜻 |
|---|---|
| TEST_CASE | 테스트 하나를 담는 매크로. 이름을 자유 문장으로 쓸 수 있고 [태그]를 붙인다. |
| SECTION | TEST_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·THEN | BDD 스타일 매크로. TEST_CASE/SECTION의 자연어 별칭. |
| Tag ([tag]) | 테스트에 붙이는 꼬리표. [a][b]=둘 다, [a],[b]=둘 중 하나, [.]=기본 실행에서 숨김. |
| AutoReg | TEST_CASE를 전역 레지스트리에 자동 등록하는 정적 초기화 객체. |
| IEventListener / Reporter | 테스트 진행 이벤트를 받아 출력을 만드는 인터페이스와 그 구현(console·junit 등 9종). |
| amalgamated | 빌드 시스템 없이 쓰도록 전체를 .hpp+.cpp 두 파일로 합친 배포본. |
| catch_discover_tests | 테스트 바이너리를 실행해 개별 TEST_CASE를 CTest에 자동 등록하는 CMake 헬퍼. |
| BSL-1.0 | Boost Software License 1.0. 상업 사용·재배포에 관대한 허용적 라이선스. |
| 구분 | 링크 |
|---|---|
| GitHub | github.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 테스트를 바로 실행 가능 |