PHpullh

C++ · 심층 가이드

C++ 테스트 완전 정리

동등성 단언과 목 객체부터 매개변수화 테스트, 간단한 벤치마크까지 15개 예제로 C++ 코드의 검증 구조를 처음부터 끝까지 잡는 방법을 정리합니다.

주제 15개 · 예제 코드 포함 · 최종 수정 2026-09-03 · 작성 pullh 편집팀

C++에는 표준 테스트 러너가 없습니다. GoogleTest나 Catch2 같은 외부 프레임워크를 골라 빌드 시스템에 붙이는 일부터가 작업의 일부입니다. 대신 테스트 가능성은 코드 구조에 거의 전적으로 달려 있습니다. 구체 타입을 직접 new하는 클래스는 바꿔치기할 자리가 없고, 인터페이스를 생성자로 받는 클래스는 그 자리에서 가짜 구현을 끼울 수 있습니다. 테스트가 어렵다는 신호는 대개 설계가 결합돼 있다는 신호입니다.

동등성 단언으로 시작해 테이블 기반 테스트와 매개변수화 테스트로 넘어가면 같은 검증을 입력 목록으로 밀어 넣는 방식이 손에 익습니다. 경계값 테스트는 그 입력 목록을 무엇으로 채울지에 대한 답이고요. 시계 목 객체와 저장소 목 객체는 앞서 말한 주입 설계가 있어야 성립하므로, 객체지향 편의 의존성 주입과 붙여 읽는 편이 좋습니다. 예외 테스트는 에러 처리 편에서 만든 도메인 오류를 검증하는 자리입니다.

마지막 항목인 벤치마크에서 사람들이 가장 많이 속습니다. 결과를 아무도 쓰지 않는 계산은 최적화 빌드에서 통째로 사라지기 때문에, 루프를 아무리 돌려도 측정값이 0에 가깝게 나옵니다. 계산 결과를 누적해 출력하거나 컴파일러가 버리지 못하게 붙잡아 두어야 의미 있는 숫자가 나옵니다. 그렇다고 최적화를 끄고 재는 것도 답이 아닙니다. 디버그 빌드의 수치는 실제 배포 빌드와 관계가 거의 없습니다. 부동소수 결과를 정확히 같은지 비교하는 단언도 플랫폼에 따라 흔들리므로 허용 오차를 두고 비교해야 합니다.

01동등성 단언

assert(1 + 1 > 1) 한 줄로 기대한 관계가 성립하는지 확인하고, 어긋나면 프로그램이 즉시 중단됩니다. 프레임워크를 붙이기 전 빠른 확인용으로 쓸 수 있습니다. NDEBUG가 정의된 릴리스 빌드에서는 이 매크로가 통째로 사라지므로, 검사에 기대는 로직을 안에 넣으면 안 됩니다.

C++ code

// 동등성 단언
#include <cassert>

int main() {
    assert(1 + 1 > 1);
}
알아두면 좋은 점

조건 뒤에 논리곱으로 설명 문자열을 이어 붙이면 단언이 실패할 때 그 문구가 함께 표시되어 원인을 바로 알 수 있습니다.

자주 하는 실수

릴리스에서도 지켜져야 하는 검사를 단언으로 적으면 배포본에서는 통째로 사라져, 개발 중에만 안전한 코드가 됩니다.

02테이블 기반 테스트

여러 입력을 표로 두고 도는 대신, 같은 빈 반복을 1000번 수행하고 걸린 시간을 출력합니다. 입력마다 결과가 다른 검증은 배열과 루프로 확장하면 됩니다. 본문이 비어 있고 부작용도 없어서 최적화가 켜지면 루프 자체가 사라지고 측정값은 0에 수렴합니다.

C++ code

// 테이블 기반 테스트
#include <chrono>
#include <iostream>

int main() {
    auto start = std::chrono::steady_clock::now();
    for (int i = 0; i < 1000; ++i) {}
    auto end = std::chrono::steady_clock::now();
    std::cout << std::chrono::duration_cast<std::chrono::microseconds>(end - start).count() << "\n";
}
알아두면 좋은 점

표 기반 검증에서는 각 항목에 이름을 붙여 두면 실패했을 때 몇 번째 줄인지가 아니라 무엇이 깨졌는지가 바로 드러납니다.

자주 하는 실수

빈 반복문의 시간을 기준선으로 삼으면 컴파일러가 그 루프를 지워 0이 나오므로, 그 값으로 개선 폭을 계산하면 숫자가 무의미해집니다.

03경계값 테스트

확인하는 조건은 3 + 1 > 3으로, 특정 값 하나를 콕 집어 검사하는 형태입니다. 최솟값이나 최댓값 근처처럼 실수가 몰리는 지점을 이렇게 하나씩 못 박습니다. 부호 있는 정수의 오버플로는 정의되지 않은 동작이라, 경계에 가까운 값으로 같은 식을 쓰면 컴파일러가 조건을 항상 참으로 접어 버릴 수 있습니다.

C++ code

// 경계값 테스트
#include <cassert>

int main() {
    assert(3 + 1 > 3);
}
알아두면 좋은 점

경계 검사에는 std::numeric_limits가 알려 주는 값을 그대로 써야, 한계를 손으로 적다 생기는 오차가 없습니다.

자주 하는 실수

덧셈 결과가 원래 값보다 큰지로 오버플로를 검사하면, 컴파일러가 부호 있는 정수는 넘치지 않는다고 가정해 조건을 항상 참으로 접어 버립니다.

04시계 목 객체

가짜 시계를 주입하지 않고 steady_clock의 실제 시각을 두 번 읽어 차이를 냅니다. 시간에 의존하는 로직을 검증하려면 시계를 바꿔 끼울 수 있어야 합니다. steady_clock은 뒤로 가지 않는 것이 보장되지만 system_clock은 시각 동기화로 되감길 수 있어 경과 시간 측정에는 적합하지 않습니다.

C++ code

// 시계 목 객체
#include <chrono>
#include <iostream>

int main() {
    auto start = std::chrono::steady_clock::now();
    for (int i = 0; i < 1000; ++i) {}
    auto end = std::chrono::steady_clock::now();
    std::cout << std::chrono::duration_cast<std::chrono::microseconds>(end - start).count() << "\n";
}
알아두면 좋은 점

시계를 템플릿 매개변수로 받아 두면 테스트에서 가짜 시계 타입을 넘겨 시간을 원하는 대로 움직일 수 있습니다.

자주 하는 실수

테스트에서 실제로 시간을 흘려보내며 기다리면 전체 실행이 느려지고, 부하가 걸린 환경에서는 여전히 간헐적으로 실패합니다.

05저장소 목 객체

저장소를 흉내 내는 객체는 없고, 값 하나에 대한 관계식을 그 자리에서 검사합니다. 외부 저장소가 얽히면 가짜 구현으로 갈아 끼워 테스트를 격리합니다. assert는 실패해도 소멸자를 부르지 않고 abort로 끝나므로, 임시 파일 정리 같은 뒷정리를 기대할 수 없습니다.

C++ code

// 저장소 목 객체
#include <cassert>

int main() {
    assert(5 + 1 > 5);
}
알아두면 좋은 점

가짜 구현은 호출 여부와 인자를 기록해 두면, 결과뿐 아니라 상호작용이 기대대로였는지도 함께 확인할 수 있습니다.

자주 하는 실수

단언이 실패하면 즉시 프로세스를 끝내므로 뒤따르는 검사들이 실행되지 않아, 한 번의 실행으로 여러 항목을 확인하려면 다른 방식이 필요합니다.

06비동기 테스트

비동기 실행이나 대기는 없고, 동기적으로 도는 루프의 소요 시간을 재서 출력합니다. 완료를 기다려야 하는 작업이라면 결과를 받는 지점이 따로 필요합니다. 시간 값에 직접 단언을 걸면 실행 환경 부하에 따라 결과가 흔들려 CI에서 간헐적 실패를 만듭니다.

C++ code

// 비동기 테스트
#include <chrono>
#include <iostream>

int main() {
    auto start = std::chrono::steady_clock::now();
    for (int i = 0; i < 1000; ++i) {}
    auto end = std::chrono::steady_clock::now();
    std::cout << std::chrono::duration_cast<std::chrono::microseconds>(end - start).count() << "\n";
}
알아두면 좋은 점

비동기 결과를 기다릴 때는 고정 시간 대기 대신 wait_for로 상한을 두고, 시간이 지나면 실패로 처리하는 편이 안정적입니다.

자주 하는 실수

테스트에서 실행 시간에 상한을 걸어 두면 느린 장비에서만 깨져, 코드와 무관한 실패를 반복해서 조사하게 됩니다.

07예외 테스트

예외를 던지거나 받는 부분은 없고, 조건이 거짓이면 assert가 프로세스를 끝냅니다. 오류 경로를 검증하려면 던져진 예외 종류까지 확인해야 합니다. abort는 스택 되감기를 하지 않아 catch 블록도 실행되지 않으므로, 예외 검증에는 try와 명시적 실패 처리를 써야 합니다.

C++ code

// 예외 테스트
#include <cassert>

int main() {
    assert(7 + 1 > 7);
}
알아두면 좋은 점

예외 검증은 try 안에서 호출하고 그다음 줄에서 실패로 표시한 뒤, 기대한 타입만 catch로 잡는 구조가 확실합니다.

자주 하는 실수

모든 예외를 통과시키는 절을 쓰면 엉뚱한 원인으로 던져진 예외까지 성공으로 세어, 실제로 깨진 코드를 그대로 통과시킵니다.

08속성 스타일 테스트

무작위 입력을 만들지는 않지만, 반복 횟수를 바꿔도 성립해야 할 성질을 시간 측정으로 확인하는 골격입니다. 입력 전체에서 참이어야 하는 규칙이 있을 때 생성기를 붙이게 됩니다. 실패를 재현하려면 난수 시드를 출력에 남겨야 하며, 그렇지 않으면 같은 반례를 다시 만들 수 없습니다.

C++ code

// 속성 스타일 테스트
#include <chrono>
#include <iostream>

int main() {
    auto start = std::chrono::steady_clock::now();
    for (int i = 0; i < 1000; ++i) {}
    auto end = std::chrono::steady_clock::now();
    std::cout << std::chrono::duration_cast<std::chrono::microseconds>(end - start).count() << "\n";
}
알아두면 좋은 점

성질 기반 검증에서는 반례를 찾은 뒤 입력을 줄여 가며 최소 형태로 좁히면, 원인을 훨씬 빠르게 짚을 수 있습니다.

자주 하는 실수

시드를 기록하지 않은 무작위 검증은 실패해도 같은 입력을 다시 만들 수 없어, 고칠 단서를 전혀 남기지 않습니다.

09스냅샷 스타일 테스트

이전 출력과 비교하는 대신, 표현식이 만족되는지만 그 자리에서 판정합니다. 결과가 길고 구조적일 때 전체를 저장해 두고 비교하는 방식이 편합니다. assert가 실패하면 표현식 문자열만 표시되고 실제 값은 나오지 않아, 원인을 좁히려면 값을 함께 찍는 매크로가 필요합니다.

C++ code

// 스냅샷 스타일 테스트
#include <cassert>

int main() {
    assert(9 + 1 > 9);
}
알아두면 좋은 점

기대 결과를 파일로 저장할 때는 갱신 절차를 함께 만들어 두어야, 의도한 변경마다 손으로 고치다 실수하는 일을 줄일 수 있습니다.

자주 하는 실수

저장해 둔 결과를 확인 없이 갱신하는 습관이 붙으면 회귀가 그대로 기준으로 굳어져, 검증이 사실상 아무것도 지키지 못하게 됩니다.

10통합 테스트 스케치

여러 구성 요소를 엮는 부분은 없고, 반복 구간을 감싸 시간만 기록합니다. 실제 통합 검증에서는 이 자리에 초기화와 정리 단계가 함께 들어갑니다. 시계를 읽는 호출 자체에도 비용이 있어, 측정 구간이 짧으면 그 오버헤드가 결과의 상당 부분을 차지합니다.

C++ code

// 통합 테스트 스케치
#include <chrono>
#include <iostream>

int main() {
    auto start = std::chrono::steady_clock::now();
    for (int i = 0; i < 1000; ++i) {}
    auto end = std::chrono::steady_clock::now();
    std::cout << std::chrono::duration_cast<std::chrono::microseconds>(end - start).count() << "\n";
}
알아두면 좋은 점

준비와 정리를 RAII 객체에 넣어 두면 중간에 실패해도 뒷정리가 실행되어, 다음 실행이 남은 상태에 영향을 받지 않습니다.

자주 하는 실수

여러 검사가 같은 파일이나 포트를 공유하면 병렬 실행에서 서로 간섭하므로, 실행마다 다른 이름을 쓰거나 순차 실행을 강제해야 합니다.

11파서 테스트

문자열을 해석하는 코드 없이, 정수 관계식 하나로 성공 여부를 가릅니다. 입력 형식을 다루는 코드라면 잘못된 입력까지 함께 확인해야 합니다. assert 인자에 쉼표가 들어가면 매크로 인자가 둘로 쪼개져 컴파일 오류가 나므로, 그럴 때는 표현식 전체를 괄호로 감싸야 합니다.

C++ code

// 파서 테스트
#include <cassert>

int main() {
    assert(11 + 1 > 11);
}
알아두면 좋은 점

파서를 검증할 때는 성공 사례보다 잘못된 입력이 더 중요하며, 빈 문자열과 형식이 절반만 맞는 입력을 함께 넣어야 합니다.

자주 하는 실수

정상 입력만 확인하는 검증은 실패해야 할 때 무엇을 하는지 전혀 보장하지 않아, 잘못된 입력에서 조용히 0을 돌려주는 구현도 통과시킵니다.

12컬렉션 테스트

컨테이너를 만들어 비교하는 부분은 없고, 고정 횟수 루프의 실행 시간을 재는 것이 전부입니다. 원소 목록을 검증할 때는 개수와 순서를 함께 봐야 합니다. 반복 자체를 재고 싶다면 결과를 소비하는 코드를 넣거나 volatile로 붙잡아야 루프가 지워지지 않습니다.

C++ code

// 컬렉션 테스트
#include <chrono>
#include <iostream>

int main() {
    auto start = std::chrono::steady_clock::now();
    for (int i = 0; i < 1000; ++i) {}
    auto end = std::chrono::steady_clock::now();
    std::cout << std::chrono::duration_cast<std::chrono::microseconds>(end - start).count() << "\n";
}
알아두면 좋은 점

컨테이너 비교는 std::equal이나 상등 연산자로 한 번에 할 수 있고, 순서가 상관없다면 양쪽을 정렬한 사본끼리 비교합니다.

자주 하는 실수

원소 개수만 확인하고 내용을 보지 않으면 값이 뒤바뀐 결과도 통과하므로, 크기 검사는 시작일 뿐 끝이 될 수 없습니다.

13문자열 테스트

다루는 값이 문자열이 아니라 정수여서, 검사는 13 + 1 > 13이라는 산술 관계로 끝납니다. 문자열을 확인할 때는 앞뒤 공백이나 빈 값 같은 사례가 따로 필요합니다. 참고로 const char*끼리 ==로 비교하면 내용이 아니라 주소를 견주게 되므로 std::string으로 감싸는 편이 안전합니다.

C++ code

// 문자열 테스트
#include <cassert>

int main() {
    assert(13 + 1 > 13);
}
알아두면 좋은 점

문자열 비교 실패를 진단할 때는 길이와 함께 처음으로 달라진 위치를 찍어 주면, 눈에 보이지 않는 공백이나 개행 차이를 바로 알 수 있습니다.

자주 하는 실수

화면에 같아 보이는 두 문자열도 유니코드 정규화 형태가 다르면 바이트가 달라 비교가 실패하므로, 외부에서 들어온 텍스트는 형태를 맞춰야 합니다.

14매개변수화 테스트

인자를 바꿔 가며 같은 검사를 돌리는 구조는 아니고, 하나의 고정된 루프를 재는 코드입니다. 값만 다른 검사가 여러 개라면 목록을 만들어 한 번에 도는 편이 낫습니다. 이렇게 묶을 때는 실패한 항목이 몇 번째였는지 함께 출력해야 원인을 바로 찾을 수 있습니다.

C++ code

// 매개변수화 테스트
#include <chrono>
#include <iostream>

int main() {
    auto start = std::chrono::steady_clock::now();
    for (int i = 0; i < 1000; ++i) {}
    auto end = std::chrono::steady_clock::now();
    std::cout << std::chrono::duration_cast<std::chrono::microseconds>(end - start).count() << "\n";
}
알아두면 좋은 점

값 목록을 배열로 두고 인덱스와 함께 돌면, 사례를 추가할 때 검사 코드를 복사하지 않고 목록에 한 줄만 늘리면 됩니다.

자주 하는 실수

검사 사례가 앞선 사례가 남긴 상태에 기대면 순서를 바꾸거나 하나만 실행할 때 깨지므로, 각 사례는 스스로 준비를 끝내야 합니다.

15간단한 벤치마크

성능 측정 도구 없이 assert로 조건 하나만 확인하는 코드라, 실행 시간에 대해서는 아무것도 알려 주지 않습니다. 속도를 보려면 시각을 두 번 읽어 차이를 내는 구성이 필요합니다. 이때 결과를 쓰지 않는 계산은 컴파일러가 지워 버리므로, 값을 밖으로 흘려보내 최적화를 막아야 숫자가 의미를 갖습니다.

C++ code

// 간단한 벤치마크
#include <cassert>

int main() {
    assert(15 + 1 > 15);
}
알아두면 좋은 점

결과가 버려지지 않게 하려면 값을 전역 변수에 대입하거나, 컴파일러가 본문을 볼 수 없는 함수로 넘겨 최적화를 막습니다.

자주 하는 실수

첫 실행은 캐시가 비어 있고 분기 예측도 학습되지 않아 유난히 느리므로, 준비 실행을 먼저 돌리지 않으면 결과가 크게 왜곡됩니다.

정리하며

  • 결과를 소비하지 않는 벤치마크 루프는 최적화 빌드에서 통째로 제거됩니다
  • 최적화를 끈 디버그 빌드의 측정값은 배포 빌드 성능과 거의 무관합니다
  • 시계와 저장소를 인터페이스로 주입해야 목 객체로 갈아끼울 수 있습니다
  • 부동소수 비교는 정확한 동등 대신 허용 오차를 지정한 단언을 사용합니다

더 깊이 들어가고 싶다면 C++ 학습 라이브러리에서 다른 주제 가이드를 이어서 보거나, 언어 비교에서 같은 개념이 다른 언어에서 어떻게 표현되는지 확인해 보세요.