Ver2.5 C++ 코드, 이제 뇌지컬 말고 시스템으로! Google Test와 TDD로 버그 없는 개발하기 🚀

C++ 코드, 이제 뇌지컬 말고 시스템으로! Google Test와 TDD로 버그 없는 개발하기 🚀
버그 잡다가 날 새는 건 이제 그만! 테스트 주도 개발(TDD)과 Google Test로
여러분의 C++ 코드를 한 단계 레벨업 시켜드릴게요. 저만 믿고 따라오세요!
들어가며: "아... 또 버그야?" 개발자의 숙명과 한 줄기 빛 ✨
우리 개발자들에게는 마치 숙명처럼 따라다니는 그림자가 있죠. 바로 '버그'입니다.
분명히 잘 돌아가던 코드였는데, 기능 하나 추가했더니 와르르 무너지는 경험. 다들 한 번쯤은 해보셨을 거예요. 😭
야심차게 리팩토링을 시작했지만, 어디서부터 잘못된 건지 알 수 없어 결국 `Ctrl+Z`를 연타하며 과거의 나 자신과 타협하기도 하죠.
이런 '코딩 불안증'의 가장 큰 원인은 무엇일까요? 바로 '확신'의 부재입니다.
내가 수정한 코드가 다른 부분에 어떤 나비효과를 일으킬지 예측할 수 없다는 공포. 이것이 우리를 소극적으로 만들고, 새로운 시도를 망설이게 합니다.
만약, 코드를 수정할 때마다 수백, 수천 개의 작은 로봇들이 내 코드의 모든 기능을 순식간에 검사해주고 "이상 없음!"이라고 알려준다면 어떨까요?
마치 든든한 아군을 등에 업고 전쟁터에 나가는 장수처럼, 우리는 훨씬 더 자신감 있게 코드를 개선하고 새로운 기능을 추가할 수 있을 겁니다.
오늘 이야기할 '유닛 테스팅(Unit Testing)'과 '테스트 주도 개발(Test-Driven Development, TDD)'이 바로 그 든든한 아군이 되어줄 시스템입니다.
그리고 C++ 진영에서 가장 강력하고 신뢰받는 무기, 'Google Test'를 다루는 방법까지 상세하게 알려드릴게요. 이제 뇌지컬과 감에 의존하는 코딩은 졸업하고, 시스템과 데이터에 기반한 스마트한 개발의 세계로 함께 떠나봅시다! 👨💻👩💻
잠깐, 용어 정리부터 할까요?
- 유닛 테스트 (Unit Test): 프로그램의 가장 작은 단위(함수, 메소드, 클래스 등)가 의도한 대로 정확히 작동하는지 검증하는 테스트입니다. 건물로 치면 벽돌 하나하나가 규격에 맞게 잘 만들어졌는지 확인하는 과정이죠.
- 테스트 주도 개발 (TDD): 실제 기능 코드를 작성하기 전에, 실패하는 테스트 케이스를 먼저 작성하는 개발 방법론입니다. '실패 -> 성공 -> 리팩토링'의 짧은 사이클을 반복하며 코드를 완성해나갑니다.
1장: 전투 준비! Google Test 개발 환경 세팅하기 🛠️
백문이 불여일견! 이론만 백날 보는 것보다 직접 부딪혀보는 게 최고죠.
우리의 든든한 무기, Google Test를 사용하기 위한 개발 환경부터 구축해봅시다. C++ 개발 환경은 이미 갖춰져 있다고 가정하고, 핵심적인 부분만 짚어드릴게요. 요즘 대세인 `CMake`를 사용해서 쉽고 깔끔하게 설정해보겠습니다.
1.1. Google Test 소스 코드 다운로드
Google Test는 구글에서 만든 오픈소스 프로젝트로, GitHub에서 소스 코드를 직접 받아 빌드해서 사용해야 합니다.
가장 일반적인 방법은 Git을 사용해 프로젝트에 `submodule`로 추가하거나, `FetchContent` 같은 CMake 기능으로 빌드 시점에 다운로드하는 것입니다. 여기서는 가장 직관적인 방법인, 직접 클론해서 사용하는 방법을 먼저 알아볼게요.
원하는 경로에 터미널을 열고 아래 명령어를 입력해 Google Test 저장소를 클론합니다.
git clone https://github.com/google/googletest.git
그러면 `googletest`라는 폴더가 생기고, 그 안에 Google Test와 Google Mock의 소스 코드가 모두 들어있을 거예요. 참 쉽죠? 😉
1.2. CMake로 Google Test 라이브러리 빌드하기
이제 다운로드한 소스 코드를 우리 프로젝트에서 사용할 수 있도록 라이브러리 파일(`.a` 또는 `.lib`)로 만들어야 합니다.
CMake를 사용하면 이 과정이 마법처럼 간단해집니다.
먼저, 우리 프로젝트의 전체적인 구조를 이렇게 잡아볼게요.
my_project/
├── CMakeLists.txt # 최상위 CMake 설정 파일
├── third_party/
│ └── googletest/ # 여기에 클론한 googletest 폴더를 이동
├── src/
│ ├── math.h
│ └── math.cpp
└── test/
├── CMakeLists.txt # 테스트용 CMake 설정 파일
└── main.cpp
이제 각 `CMakeLists.txt` 파일을 작성해봅시다.
1. 최상위 `my_project/CMakeLists.txt`
이 파일은 프로젝트 전체를 총괄하는 역할을 합니다.
cmake_minimum_required(VERSION 3.14)
project(MyAwesomeProject)
# C++17 표준을 사용하도록 설정
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Google Test 빌드를 위해 테스트 옵션을 활성화
enable_testing()
# third_party/googletest 폴더를 CMake 빌드에 추가
# EXCLUDE_FROM_ALL 옵션으로 기본 빌드 시에는 googletest 자체를 빌드하지 않도록 설정
add_subdirectory(third_party/googletest EXCLUDE_FROM_ALL)
# 우리 소스 코드가 있는 src 폴더를 빌드에 추가
add_subdirectory(src)
# 우리 테스트 코드가 있는 test 폴더를 빌드에 추가
add_subdirectory(test)
2. 테스트 폴더 `my_project/test/CMakeLists.txt`
이 파일은 실제 테스트 코드를 빌드하고 Google Test 라이브러리와 연결하는 핵심적인 역할을 합니다.
# 테스트 실행 파일의 이름을 'runTests'로 지정
add_executable(runTests main.cpp)
# 우리 프로젝트의 소스 코드 라이브러리(src 폴더에서 생성했다고 가정)와
# Google Test 라이브러리를 'runTests'에 연결합니다.
# gtest_main은 main 함수까지 포함해줘서 편리해요.
target_link_libraries(runTests PRIVATE MyProjectLib gtest_main)
# CTest에 테스트를 추가하여 'ctest' 명령어로 실행할 수 있게 함
include(GoogleTest)
gtest_discover_tests(runTests)
💡 프로팁: `FetchContent`로 더 깔끔하게!
프로젝트에 `googletest` 소스를 직접 복사해두는 게 부담스럽다면, CMake의 `FetchContent` 모듈을 사용해보세요. `CMakeLists.txt`에 몇 줄만 추가하면 빌드 시점에 알아서 Git 저장소에서 코드를 다운로드하고 빌드까지 해줍니다. 프로젝트가 훨씬 깔끔해지겠죠?
# 최상위 CMakeLists.txt에 추가
include(FetchContent)
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG release-1.12.1
)
FetchContent_MakeAvailable(googletest)
이렇게 하면 `third_party` 폴더 없이도 `target_link_libraries`에서 `gtest_main`을 바로 사용할 수 있게 됩니다. 완전 꿀팁이죠?
1.3. 첫 번째 테스트: "Hello, Test World!"
모든 준비가 끝났습니다! 이제 우리의 개발 환경이 잘 구축되었는지 확인하기 위해, 세상에서 가장 간단한 테스트 코드를 작성해봅시다.
`test/main.cpp` 파일을 열고 아래와 같이 작성해주세요.
#include <gtest/gtest.h>
// 첫 번째 테스트 케이스
// TEST(테스트_스위트_이름, 테스트_이름)
TEST(HelloTest, BasicAssertions) {
// 기본적인 기대를 테스트합니다.
EXPECT_STRNE("hello", "world"); // 두 C-style 문자열이 다른지 확인
EXPECT_EQ(7 * 6, 42); // 두 값이 같은지 확인
}
// main 함수는 gtest_main을 링크하면 자동으로 생성되므로,
// 직접 작성할 필요가 없습니다. 하지만 커스텀이 필요하다면
// 아래와 같이 직접 작성하고 gtest가 아닌 gtest_main을 링크해야 합니다.
/*
int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
*/
이제 프로젝트 루트 디렉토리(`my_project/`)에서 터미널을 열고 CMake로 빌드하고 테스트를 실행해봅시다.
# 빌드 디렉토리 생성 및 이동
mkdir build
cd build
# CMake 실행하여 빌드 시스템 생성
cmake ..
# 빌드 실행
cmake --build .
# CTest로 테스트 실행
ctest
모든 것이 정상이라면, 터미널에 초록색 글씨로 테스트가 통과했다는 멋진 메시지가 나타날 겁니다! 🎉
[==========] Running 1 test from 1 test suite.
[----------] Global test environment set-up.
[----------] 1 test from HelloTest
[ RUN ] HelloTest.BasicAssertions
[ OK ] HelloTest.BasicAssertions (0 ms)
[----------] 1 test from HelloTest (0 ms total)
[----------] Global test environment tear-down
[==========] 1 test from 1 test suite ran. (0 ms total)
[ PASSED ] 1 test.
축하합니다! 여러분은 이제 C++ 프로젝트에 Google Test라는 강력한 무기를 장착했습니다. 이제 이 무기를 어떻게 사용하는지 본격적으로 탐험해볼 시간입니다.
2장: 이것만 알면 반은 성공! Google Test 핵심 어설션(Assertion) 완전 정복 🎯
테스트 코드의 심장은 바로 '어설션(Assertion)'입니다. 우리말로는 '단언'이나 '주장' 정도로 번역할 수 있는데, 쉽게 말해 "이 코드는 반드시 이렇게 동작해야 해!"라고 강력하게 주장하는 코드 조각입니다.
Google Test는 정말 풍부하고 직관적인 어설션 매크로들을 제공합니다. 이 매크로들만 잘 알아둬도 웬만한 테스트는 다 작성할 수 있어요. 이번 장에서는 가장 핵심적인 어설션들을 집중적으로 파헤쳐 보겠습니다.
2.1. `ASSERT_*` vs `EXPECT_*`: 치명적 실패 vs 일반 실패
Google Test의 어설션은 크게 두 가지 계열로 나뉩니다. `ASSERT_`로 시작하는 것과 `EXPECT_`로 시작하는 것.
이 둘의 차이는 아주 중요하니 꼭 기억해주세요!
- `EXPECT_*` (기대): 어설션이 실패하더라도 테스트 함수를 중단하지 않고 계속 실행합니다. 하나의 테스트 케이스에서 여러 개의 상태를 검증하고 싶을 때 유용합니다. "이게 맞았으면 좋겠어. 틀려도 일단 다른 것도 확인해볼게." 라는 뉘앙스죠.
- `ASSERT_*` (단언): 어설션이 실패하면 현재 테스트 함수를 즉시 중단합니다. 이후의 코드는 실행되지 않아요. 테스트의 전제 조건이 무너져서 더 이상 진행하는 의미가 없을 때 사용합니다. "이건 무조건 맞아야 해! 틀리면 더 볼 것도 없어." 라는 강력한 주장입니다.
언제 무엇을 써야 할까요? 🤔
일반적으로는 `EXPECT_*`를 사용하는 것이 좋습니다. 하나의 테스트가 실패했다고 다른 검증 기회까지 놓치면, 버그의 전체적인 상황을 파악하기 어렵기 때문입니다.
`ASSERT_*`는 포인터가 `nullptr`이 아니어야 하는 상황처럼, 해당 조건이 만족되지 않으면 이후 코드가 무조건 비정상 종료되는 경우에만 제한적으로 사용하는 것을 추천합니다.
예시를 통해 차이점을 명확히 봅시다.
TEST(AssertionTest, ExpectVsAssert) {
EXPECT_EQ(1, 2); // 실패! 하지만 계속 진행
std::cout << "This line will be printed." << std::endl;
ASSERT_EQ(1, 2); // 실패! 여기서 함수 즉시 종료
std::cout << "This line will NEVER be printed." << std::endl;
}
위 테스트를 실행하면 "This line will be printed."는 출력되지만, "This line will NEVER be printed."는 절대 출력되지 않는 것을 확인할 수 있습니다.
2.2. 기본 어설션: 진실 혹은 거짓, 그리고 동등성 비교
가장 많이 사용되는 기본적인 어설션들입니다. 마치 게임의 기본 공격 스킬 같죠!
2.2.1. 참/거짓 검증
가장 단순한 형태의 검증입니다. 조건이 참인지 거짓인지를 확인합니다.
bool isEven(int n) { return n % 2 == 0; }
TEST(BooleanTest, IsEven) {
EXPECT_TRUE(isEven(4));
EXPECT_FALSE(isEven(5));
// ASSERT 버전도 당연히 있습니다.
ASSERT_TRUE(isEven(100));
}
2.2.2. 값 비교 검증
두 값이 같은지, 다른지, 혹은 누가 더 큰지를 비교합니다. 아마 가장 많이 사용하게 될 어설션일 겁니다.
TEST(ComparisonTest, NumericValues) {
EXPECT_EQ(2 + 2, 4); // Equal (==)
EXPECT_NE(2 + 2, 5); // Not Equal (!=)
EXPECT_LT(3, 4); // Less Than (<)
EXPECT_LE(3, 3); // Less or Equal (<=)
EXPECT_GT(5, 4); // Greater Than (>)
EXPECT_GE(5, 5); // Greater or Equal (>=)
}
💡 `EXPECT_EQ`의 똑똑한 실패 메시지
`EXPECT_TRUE(a == b)` 대신 `EXPECT_EQ(a, b)`를 사용하는 것을 강력히 추천합니다. 왜냐하면 `EXPECT_EQ`는 실패했을 때 두 값이 무엇이었는지 아주 친절하게 알려주거든요!
`EXPECT_TRUE(1 == 2);` -> 실패 메시지: `Value of: 1 == 2` is `false`
`EXPECT_EQ(1, 2);` -> 실패 메시지: `Expected: 1` vs `Actual: 2`
어떤가요? 두 번째 메시지가 훨씬 디버깅하기 쉽겠죠? 😉
2.3. 문자열 비교: C-style vs C++ `std::string`
문자열 비교는 C++에서 조금 까다로운 부분이죠. C-style 문자열(`const char*`)과 C++ `std::string` 객체를 다루는 방법이 다르기 때문입니다. Google Test는 이 둘을 모두 우아하게 처리할 수 있는 어설션을 제공합니다.
TEST(StringTest, CStyleAndStdString) {
const char* c_str1 = "hello";
const char* c_str2 = "hello";
const char* c_str3 = "world";
// C-style 문자열 비교: 포인터 주소가 아닌 실제 내용을 비교합니다.
EXPECT_STREQ(c_str1, c_str2);
EXPECT_STRNE(c_str1, c_str3);
// 대소문자를 무시하고 비교하고 싶을 때
EXPECT_STRCASEEQ("HELLO", "hello");
EXPECT_STRCASENE("HELLO", "world");
// std::string 객체는 일반 비교 어설션을 사용하면 됩니다.
std::string s1 = "hello";
std::string s2 = "hello";
std::string s3 = "world";
EXPECT_EQ(s1, s2);
EXPECT_NE(s1, s3);
}
주의! C-style 문자열에 `EXPECT_EQ`를 사용하면 포인터의 주소값을 비교하게 되므로, 의도치 않은 테스트 실패를 겪을 수 있습니다. C-style 문자열은 반드시 `EXPECT_STREQ` 계열을 사용해주세요!
2.4. 부동소수점 비교: 보이지 않는 오차와의 싸움
컴퓨터에서 부동소수점(float, double) 연산은 미세한 오차를 포함하는 경우가 많습니다. `0.1 + 0.2`가 정확히 `0.3`이 아닐 수도 있다는 사실! 😱
따라서 `EXPECT_EQ`로 부동소수점을 비교하는 것은 매우 위험한 생각입니다.
Google Test는 이런 문제를 해결하기 위한 전용 어설션을 제공합니다.
TEST(FloatingPointTest, AvoidEQ) {
double d1 = 0.1 + 0.2;
double d2 = 0.3;
// 이렇게 하면 실패할 가능성이 매우 높습니다!
// EXPECT_EQ(d1, d2);
// float 타입 비교
EXPECT_FLOAT_EQ(3.0f, 3.0f);
// double 타입 비교
EXPECT_DOUBLE_EQ(d1, d2); // 이것도 내부적으로는 오차를 감안하지만...
// 가장 안전한 방법: 허용 오차를 직접 지정
// d1과 d2의 차이가 0.00001 이내라면 성공으로 간주
EXPECT_NEAR(d1, d2, 0.00001);
}
`EXPECT_FLOAT_EQ`와 `EXPECT_DOUBLE_EQ`는 내부적으로 4 ULP(Units in the Last Place)까지의 오차를 허용해줍니다. 하지만 더 정밀한 제어가 필요하거나, 오차의 의미를 명확하게 하고 싶다면 `EXPECT_NEAR`를 사용하는 것이 가장 좋습니다.
2.5. 예외 발생 검증: 예상된 비상 상황 테스트하기
잘 만들어진 코드는 비정상적인 상황에서 적절한 예외(Exception)를 던져야 합니다.
Google Test를 사용하면 특정 코드가 우리가 예상한 종류의 예외를 던지는지 쉽게 검증할 수 있습니다.
#include <stdexcept>
void functionThatThrows() {
throw std::runtime_error("Something bad happened");
}
int divide(int a, int b) {
if (b == 0) {
throw std::invalid_argument("Division by zero!");
}
return a / b;
}
TEST(ExceptionTest, ThrowsCorrectException) {
// functionThatThrows() 함수가 std::runtime_error 예외를 던지는지 확인
EXPECT_THROW(functionThatThrows(), std::runtime_error);
// divide(1, 0) 코드가 std::invalid_argument 예외를 던지는지 확인
EXPECT_THROW(divide(1, 0), std::invalid_argument);
// 어떤 종류든 예외가 발생하기만 하면 되는지 확인
EXPECT_ANY_THROW(divide(5, 0));
// 예외가 발생하지 *않는지* 확인
EXPECT_NO_THROW(divide(10, 2));
}
2.6. 실패 메시지 커스터마이징
때로는 Google Test가 제공하는 기본 실패 메시지로는 부족할 때가 있습니다. 테스트가 왜 실패했는지 더 자세한 문맥을 추가하고 싶을 때, `<<` 연산자를 사용해 커스텀 메시지를 덧붙일 수 있습니다.
TEST(CustomMessageTest, AddContextToFailure) {
for (int i = 0; i < 5; ++i) {
// 실패 시, 어떤 i 값에서 문제가 발생했는지 알려줌
EXPECT_TRUE(i < 4) << "Loop failed at iteration i = " << i;
}
}
위 테스트는 `i`가 4일 때 실패하며, 다음과 같은 아주 친절한 메시지를 남길 겁니다.
`Value of: i < 4 is false. Loop failed at iteration i = 4`
어떤가요? 이 정도 어설션만 자유자재로 다룰 수 있어도 여러분의 테스트 코드 작성 능력은 이미 중급 이상이라고 할 수 있습니다. 이제 이 어설션들을 더 체계적으로 관리하고 재사용성을 높이는 방법을 알아볼 시간입니다. 바로 '테스트 픽스처'의 세계로 떠나봅시다!
3장: 코드는 깔끔하게, 테스트는 우아하게: 테스트 픽스처(Test Fixture) 활용법 🏛️
여러 개의 테스트를 작성하다 보면, 공통적으로 필요한 작업들이 생기기 마련입니다.
예를 들어, `BankAccount` 클래스를 테스트한다고 상상해봅시다. 입금 테스트, 출금 테스트, 잔액 조회 테스트 등 모든 테스트를 시작하기 전에 `BankAccount` 객체를 생성하고 초기 잔액을 설정하는 작업이 필요하겠죠. 테스트가 끝난 후에는 생성했던 객체를 정리해야 할 수도 있습니다.
이런 공통적인 준비(Set-up) 및 정리(Tear-down) 작업을 모든 테스트 케이스마다 복사-붙여넣기 한다면? 끔찍하죠. 🤢 코드는 중복되고, 유지보수는 어려워질 겁니다. 바로 이럴 때 구원투수처럼 등장하는 것이 '테스트 픽스처(Test Fixture)'입니다.
3.1. 테스트 픽스처란?
테스트 픽스처는 여러 테스트에서 공통으로 사용하는 데이터와 객체의 집합, 그리고 그것들을 준비하고 정리하는 로직을 하나로 묶은 클래스입니다.
Google Test에서는 `::testing::Test` 클래스를 상속받아 우리만의 픽스처 클래스를 만들 수 있습니다.
이 픽스처 클래스 안에는 특별한 의미를 갖는 두 개의 메소드가 있습니다.
- `SetUp()`: 각 테스트가 시작되기 직전에 호출됩니다. 테스트에 필요한 객체를 생성하고, 초기화하는 작업을 여기에 넣습니다.
- `TearDown()`: 각 테스트가 종료된 직후에 호출됩니다. `SetUp()`에서 할당한 리소스를 해제하는 작업을 주로 담당합니다.
중요한 점은 `SetUp()`과 `TearDown()`은 픽스처를 사용하는 모든 테스트 케이스마다 각각 실행된다는 것입니다. 덕분에 각 테스트는 서로에게 영향을 주지 않는 깨끗한 환경에서 독립적으로 실행될 수 있습니다. 이것이 바로 테스트의 '독립성(Isolation)'을 보장하는 핵심 원리입니다.
3.2. `TEST_F` 매크로와 함께하는 픽스처 실전 예제
백문이 불여일견! 간단한 `BankAccount` 클래스를 만들고, 테스트 픽스처를 사용해 테스트 코드를 작성해봅시다.
1. 테스트 대상 클래스 (`BankAccount.h`)
먼저 테스트할 간단한 은행 계좌 클래스를 정의합니다.
#pragma once
#include <stdexcept>
class BankAccount {
private:
int balance_ = 0;
public:
BankAccount() = default;
explicit BankAccount(int initial_balance) : balance_(initial_balance) {}
int get_balance() const {
return balance_;
}
void deposit(int amount) {
if (amount <= 0) {
throw std::invalid_argument("Amount must be positive");
}
balance_ += amount;
}
void withdraw(int amount) {
if (amount <= 0) {
throw std::invalid_argument("Amount must be positive");
}
if (balance_ < amount) {
throw std::runtime_error("Insufficient funds");
}
balance_ -= amount;
}
};
2. 테스트 픽스처 클래스 정의 (`BankAccountTest.cpp`)
이제 `BankAccount`를 테스트하기 위한 픽스처를 만듭니다. `::testing::Test`를 상속받는 것을 잊지 마세요!
#include <gtest/gtest.h>
#include "BankAccount.h"
// BankAccountTest 라는 이름의 테스트 픽스처 클래스를 정의합니다.
class BankAccountTest : public ::testing::Test {
protected:
// 이 픽스처를 사용하는 모든 테스트에서 접근할 수 있도록 protected 멤버로 선언합니다.
BankAccount* account;
// 각 테스트 시작 전에 호출됩니다.
void SetUp() override {
std::cout << "SetUp() called!" << std::endl;
account = new BankAccount(100); // 100의 초기 잔액을 가진 계좌 생성
}
// 각 테스트 종료 후에 호출됩니다.
void TearDown() override {
std::cout << "TearDown() called!" << std::endl;
delete account;
account = nullptr;
}
};
이제 `account` 객체는 모든 테스트에서 `SetUp()`을 통해 100의 잔액을 가진 상태로 준비됩니다. 테스트가 끝나면 `TearDown()`이 깔끔하게 메모리를 해제해주고요.
3. `TEST_F` 매크로로 테스트 케이스 작성
픽스처를 사용하는 테스트 케이스는 일반 `TEST` 매크로 대신 `TEST_F` 매크로를 사용합니다. 여기서 'F'는 Fixture를 의미합니다.
`TEST_F`의 첫 번째 인자로는 우리가 만든 픽스처 클래스의 이름을 넣어줍니다.
// TEST_F(픽스처_이름, 테스트_이름)
TEST_F(BankAccountTest, InitialBalanceIsSetCorrectly) {
// SetUp()에서 생성된 account 객체에 바로 접근할 수 있습니다.
EXPECT_EQ(account->get_balance(), 100);
}
TEST_F(BankAccountTest, CanDepositMoney) {
account->deposit(50);
EXPECT_EQ(account->get_balance(), 150);
}
TEST_F(BankAccountTest, CanWithdrawMoney) {
account->withdraw(30);
EXPECT_EQ(account->get_balance(), 70);
}
TEST_F(BankAccountTest, ThrowsOnInsufficientFunds) {
// 100보다 많은 금액을 출금하려고 하면 std::runtime_error 예외가 발생하는지 확인
EXPECT_THROW(account->withdraw(200), std::runtime_error);
}
TEST_F(BankAccountTest, ThrowsOnNegativeDeposit) {
EXPECT_THROW(account->deposit(-50), std::invalid_argument);
}
위 테스트를 실행하면, 각 `TEST_F` 케이스가 실행되기 전에 "SetUp() called!"가, 끝난 후에 "TearDown() called!"가 출력되는 것을 볼 수 있습니다. 각 테스트는 항상 잔액 100에서 시작하므로, `CanDepositMoney` 테스트에서 150이 된 잔액이 `CanWithdrawMoney` 테스트에 전혀 영향을 주지 않습니다. 완벽한 독립성이죠! ✨
픽스처의 생명주기 (Lifecycle)
1. Google Test가 `BankAccountTest` 객체를 생성합니다. (생성자 호출)
2. `SetUp()` 메소드를 호출합니다.
3. 테스트 케이스 본문(`TEST_F`의 중괄호 안 코드)을 실행합니다.
4. `TearDown()` 메소드를 호출합니다.
5. `BankAccountTest` 객체를 파괴합니다. (소멸자 호출)
이 1~5번 과정이 `BankAccountTest` 픽스처를 사용하는 모든 테스트 케이스마다 반복됩니다.
테스트 픽스처는 단순히 코드 중복을 줄여주는 것을 넘어, 테스트의 독립성을 보장하고, 복잡한 객체들의 생성 및 초기화 과정을 한 곳에서 관리하게 해줌으로써 테스트 코드의 전체적인 구조를 매우 깔끔하고 이해하기 쉽게 만들어줍니다. 여러분의 프로젝트가 복잡해질수록 픽스처의 가치는 더욱 빛나게 될 겁니다.
4장: 백문이 불여일견! TDD 사이클 직접 돌려보기 (Red-Green-Refactor) 🔴🟢🔵
지금까지 우리는 테스트 코드를 작성하는 '방법'에 대해 배웠습니다. 이제부터는 테스트를 '언제' 작성해야 하는지에 대한 이야기, 즉 테스트 주도 개발(TDD)의 세계로 뛰어들 시간입니다.
TDD의 핵심은 아주 간단한 세 단계의 사이클을 반복하는 것입니다.
- RED: 실패하는 테스트 코드를 먼저 작성한다. (아직 기능이 없으니 당연히 실패하겠죠?)
- GREEN: 이 테스트를 통과시키는 가장 간단한 코드를 작성한다. (일단 돌아가게만 만든다!)
- REFACTOR: 방금 작성한 코드를 (테스트가 계속 통과하는 것을 유지하면서) 개선한다.
이 사이클은 마치 아기가 걸음마를 배우는 과정과 같습니다. 넘어지고(RED), 일어서고(GREEN), 더 잘 걷기 위해 자세를 교정하는(REFACTOR) 과정의 반복이죠.
이 과정을 통해 우리는 항상 '동작하는 코드'라는 안전망 위에서 점진적으로 소프트웨어를 개발해나갈 수 있습니다.
말로만 들으면 감이 잘 안 오죠? 간단한 '문자열 계산기'를 TDD 방식으로 직접 만들어보면서 이 리듬을 몸으로 느껴봅시다!
요구사항: 문자열 계산기 만들기
우리가 만들 `StringCalculator` 클래스는 `add`라는 메소드를 가집니다. 이 메소드는 쉼표(`,`)로 구분된 숫자가 담긴 문자열을 입력받아 그 합계를 반환합니다.
- 예: `"1,2"` -> `3`
- 예: `"10,20,30"` -> `60`
Step 1: RED - 가장 간단한 케이스, 빈 문자열
가장 먼저, 가장 간단하고 극단적인 케이스부터 생각합니다. "빈 문자열을 입력하면 어떻게 되어야 할까?"
상식적으로 합계는 0이 되어야겠죠. 이 요구사항을 테스트 코드로 먼저 작성해봅시다.
#include <gtest/gtest.h>
// #include "StringCalculator.h" // 아직 파일이 없어서 주석 처리!
// TEST(StringCalculatorTest, EmptyStringReturnsZero) {
// StringCalculator calc;
// EXPECT_EQ(calc.add(""), 0);
// }
이 코드를 작성하는 순간, 컴파일러는 `StringCalculator`가 뭐냐고 화를 낼 겁니다. 당연하죠. 아직 만들지 않았으니까요. 이것이 TDD의 첫 번째 RED 상태입니다. (컴파일 실패도 실패입니다!)
Step 2: GREEN - 컴파일 에러를 해결하고 테스트를 통과시키자
이제 이 컴파일 에러를 해결하기 위한 최소한의 코드를 작성합니다.
#pragma once
#include <string>
class StringCalculator {
public:
int add(const std::string& input);
};
#include "StringCalculator.h"
int StringCalculator::add(const std::string& input) {
return -1; // 일단 아무 값이나 리턴
}
이제 테스트 코드의 주석을 풀고 다시 빌드하면, 컴파일은 성공할 겁니다. 하지만 테스트를 실행하면? `EXPECT_EQ(calc.add(""), 0)`에서 `-1`과 `0`이 다르므로 테스트는 실패합니다. 이것이 두 번째 RED 상태입니다.
이제 이 테스트를 통과시킬 가장 간단한 코드는 무엇일까요?
#include "StringCalculator.h"
int StringCalculator::add(const std::string& input) {
if (input.empty()) {
return 0;
}
return -1; // 다른 경우는 아직 모르니 그대로 둠
}
다시 테스트를 실행하면... 짜잔! 테스트가 통과합니다! 우리는 첫 번째 GREEN 상태에 도달했습니다. ✅
Step 3: REFACTOR - 개선할 게 있을까?
지금 코드는 너무 간단해서 딱히 개선할 부분이 없어 보입니다. 이럴 땐 그냥 다음 단계로 넘어갑니다. 리팩토링은 필요할 때만 하는 것입니다.
TDD 사이클 반복: 숫자 하나
[RED] 다음으로 간단한 케이스는 숫자 하나만 있는 경우입니다. `"1"`을 넣으면 `1`이 나와야겠죠. 이 테스트를 추가합니다.
TEST(StringCalculatorTest, SingleNumberReturnsItself) {
StringCalculator calc;
EXPECT_EQ(calc.add("5"), 5);
}
기존 코드는 빈 문자열이 아니면 `-1`을 반환하므로, 이 테스트는 당연히 실패합니다. RED!
[GREEN] 이 테스트를 통과시킬 가장 간단한 방법은? 그냥 문자열을 정수로 바꾸어 반환하는 것입니다.
#include "StringCalculator.h"
#include <string> // for std::stoi
int StringCalculator::add(const std::string& input) {
if (input.empty()) {
return 0;
}
// C++11의 std::stoi를 사용해 문자열을 정수로 변환
return std::stoi(input);
}
이제 모든 테스트(빈 문자열, 숫자 하나)를 다시 실행합니다. 모두 통과하네요! GREEN!
[REFACTOR] 아직 코드는 깔끔합니다. 다음으로 넘어갑시다.
TDD 사이클 반복: 숫자 두 개
[RED] 드디어 핵심 기능인 쉼표로 구분된 두 숫자의 합계를 테스트할 차례입니다. `"1,2"`는 `3`을 반환해야 합니다.
TEST(StringCalculatorTest, TwoNumbersReturnTheirSum) {
StringCalculator calc;
EXPECT_EQ(calc.add("1,2"), 3);
EXPECT_EQ(calc.add("10,20"), 30);
}
현재 코드는 `std::stoi("1,2")`를 시도할 것이고, 쉼표 때문에 예외가 발생하거나 `1`만 변환되어 테스트는 실패할 겁니다. RED!
[GREEN] 이제 진짜 로직이 필요합니다. 쉼표를 찾아 문자열을 자르고, 각 숫자를 더해야 합니다. 일단 돌아가게만 만들어 봅시다.
#include "StringCalculator.h"
#include <string>
#include <sstream> // for std::stringstream
int StringCalculator::add(const std::string& input) {
if (input.empty()) {
return 0;
}
std::stringstream ss(input);
std::string segment;
int sum = 0;
// 쉼표(,)를 기준으로 문자열을 분리
while(std::getline(ss, segment, ',')) {
sum += std::stoi(segment);
}
return sum;
}
모든 테스트를 다시 실행합니다. 빈 문자열, 숫자 하나, 숫자 두 개... 모두 통과합니다! GREEN! 심지어 이 코드는 숫자 세 개, 네 개... 여러 개인 경우도 처리할 수 있겠네요!
[REFACTOR] 이제 코드를 살펴봅시다. `stringstream`을 사용하는 로직은 꽤 괜찮아 보입니다. 변수 이름도 명확하고, 특별히 개선할 부분은 없어 보입니다. 하지만 만약 코드가 더 복잡했다면, 이 단계에서 함수를 분리하거나, 변수명을 더 명확하게 바꾸는 등의 작업을 했을 겁니다.
TDD가 주는 선물 🎁
어떤가요? TDD 사이클을 따라가다 보니, 우리는 한 번에 하나의 문제에만 집중할 수 있었습니다. '빈 문자열 처리'에 집중할 때는 '숫자 파싱'을 걱정할 필요가 없었죠. 이렇게 복잡한 문제를 아주 작은 단위로 나누어 하나씩 해결해나가기 때문에, 최종적으로 완성된 코드는 견고하고 모든 요구사항을 만족하게 됩니다.
또한, 이 과정에서 만들어진 테스트 코드들은 앞으로 이 코드를 수정하거나 새로운 기능을 추가할 때마다 우리의 코드가 망가지지 않았다는 것을 보장해주는 든든한 '회귀 테스트(Regression Test)' 스위트가 됩니다. 이것이 TDD가 주는 가장 큰 선물입니다.
이런 식으로 TDD는 '다음엔 뭘 해야 하지?'라는 막막함 대신, '다음엔 어떤 테스트를 추가할까?'라는 명확한 목표를 제시하며 개발 과정을 이끌어줍니다. 여러분도 다음 프로젝트에서는 작은 기능 하나라도 TDD 사이클을 직접 경험해보세요. 코딩에 대한 자신감이 한 단계 올라가는 것을 느끼실 수 있을 겁니다!
5장: 고수의 길로: Google Test 고급 기능 파헤치기 🧙♂️
기본적인 어설션과 픽스처, 그리고 TDD의 리듬까지 익혔다면 여러분은 이미 유닛 테스트의 기본기를 탄탄히 다진 셈입니다. 하지만 Google Test의 진정한 힘은 여기서 그치지 않습니다. 더 복잡하고 까다로운 상황을 우아하게 해결해 줄 강력한 고급 기능들이 숨어있죠. 이번 장에서는 여러분을 '테스트 고수'의 반열에 올려줄 몇 가지 비급을 전수해 드리겠습니다.
5.1. 파라미터화 테스트 (Parameterized Tests): 데이터만 바꿔서 계속 테스트하기
똑같은 테스트 로직을 입력값과 기대값만 바꿔서 여러 번 실행하고 싶을 때가 있습니다.
예를 들어, '소수(prime number) 판별 함수'를 테스트한다고 해봅시다. 2, 3, 5, 7, 11은 소수이고, 4, 6, 8, 9, 10은 소수가 아니라는 것을 검증해야겠죠. 이걸 각각의 `TEST` 케이스로 만들면 코드가 엄청나게 중복될 겁니다.
이럴 때 사용하는 것이 바로 '파라미터화 테스트'입니다. 테스트 로직은 한 번만 작성하고, 테스트에 사용할 데이터(파라미터) 목록만 제공하면 Google Test가 알아서 각 데이터에 대해 테스트를 실행해줍니다.
파라미터화 테스트를 작성하는 3단계:
- `::testing::TestWithParam
`를 상속받는 픽스처 클래스를 만듭니다. `T`는 파라미터의 타입입니다. (예: `int`, `std::pair `) - `TEST_P` 매크로를 사용해 테스트 코드를 작성합니다. `P`는 Parameterized의 약자입니다. 테스트 코드 안에서는 `GetParam()` 메소드로 현재 테스트에 사용되는 파라미터를 가져올 수 있습니다.
- `INSTANTIATE_TEST_SUITE_P` 매크로를 사용해 테스트 스위트에 테스트할 데이터 목록을 등록합니다.
소수 판별 함수 `isPrime`을 테스트하는 예제를 봅시다.
#include <gtest/gtest.h>
#include <vector>
// 테스트 대상 함수 (간단한 구현)
bool isPrime(int n) {
if (n <= 1) return false;
for (int i = 2; i * i <= n; ++i) {
if (n % i == 0) return false;
}
return true;
}
// 1. ::testing::TestWithParam<int>를 상속받는 픽스처
class PrimeTest : public ::testing::TestWithParam<int> {
// 픽스처 내용은 비어있어도 됩니다.
};
// 2. TEST_P 매크로로 테스트 로직 작성
TEST_P(PrimeTest, HandlesPrimeNumbers) {
int n = GetParam(); // GetParam()으로 데이터 가져오기
EXPECT_TRUE(isPrime(n)) << n << " should be a prime number.";
}
// 3. INSTANTIATE_TEST_SUITE_P로 데이터 목록 등록
INSTANTIATE_TEST_SUITE_P(
PrimeNumbers, // 인스턴스 이름 (아무거나)
PrimeTest, // 픽스처 클래스 이름
::testing::Values(2, 3, 5, 7, 11, 13, 17, 19) // 테스트할 값들
);
// 소수가 아닌 경우에 대한 테스트도 똑같이 만들 수 있습니다.
class NonPrimeTest : public ::testing::TestWithParam<int> {};
TEST_P(NonPrimeTest, HandlesNonPrimeNumbers) {
int n = GetParam();
EXPECT_FALSE(isPrime(n)) << n << " should not be a prime number.";
}
INSTANTIATE_TEST_SUITE_P(
NonPrimeNumbers,
NonPrimeTest,
::testing::Values(1, 4, 6, 8, 9, 10, 12, 14, 15, 16, 18, 20)
);
위 코드를 실행하면 Google Test는 `PrimeNumbers` 그룹에 대해 8개의 테스트를, `NonPrimeNumbers` 그룹에 대해 12개의 테스트를 자동으로 생성하고 실행합니다. 데이터가 추가되거나 변경되어도 테스트 로직은 수정할 필요가 없으니 정말 편리하죠?
5.2. 죽음 테스트 (Death Tests): 예상된 비정상 종료 검증하기
때로는 프로그램이 특정 상황에서 '반드시 죽어야(비정상 종료되어야)' 하는 경우가 있습니다.
예를 들어, `assert` 매크로가 실패하는 경우, 혹은 `nullptr`에 접근하여 세그멘테이션 폴트(segmentation fault)가 발생하는 경우 등이죠. 이런 '예상된 죽음'을 테스트하는 것이 바로 '죽음 테스트'입니다.
죽음 테스트는 `ASSERT_DEATH`나 `EXPECT_DEATH` 매크로를 사용합니다. 이 매크로들은 주어진 코드를 별도의 자식 프로세스에서 실행하고, 그 프로세스가 예상대로 종료되는지를 확인합니다.
⚠️ 경고: 죽음 테스트는 느립니다! 죽음 테스트는 내부적으로 새로운 프로세스를 생성(`fork`)하기 때문에 일반적인 테스트보다 훨씬 느립니다. 따라서 꼭 필요한 경우에만 제한적으로 사용해야 합니다. 가능하면 예외(Exception)를 던지도록 코드를 설계하고 `EXPECT_THROW`로 테스트하는 것이 더 좋은 방법입니다.
간단한 예제를 살펴봅시다.
#include <gtest/gtest.h>
#include <cassert>
void functionWithAssert(int* ptr) {
assert(ptr != nullptr); // ptr이 nullptr이면 여기서 프로그램이 죽습니다.
// ...
}
TEST(DeathTest, AssertFiresOnNull) {
int* p = nullptr;
// functionWithAssert(p)를 실행하면 "ptr != nullptr" 라는 메시지와 함께 죽는지 확인
ASSERT_DEATH(functionWithAssert(p), "ptr != nullptr");
}
void functionThatCrashes() {
int* p = nullptr;
*p = 10; // 세그멘테이션 폴트!
}
TEST(DeathTest, CatchesSegfault) {
// 어떤 이유로든 죽기만 하면 되는지 확인하려면, 정규표현식으로 "" (빈 문자열)을 줍니다.
EXPECT_DEATH(functionThatCrashes(), "");
}
죽음 테스트는 방어적인 프로그래밍에서 설정한 전제 조건(precondition)들이 실제로 잘 동작하는지 검증하는 데 매우 유용합니다.
5.3. 맛보기: Google Mock으로 의존성 분리하기
지금까지 우리가 테스트한 코드들은 대부분 외부 의존성이 없는 간단한 함수나 클래스였습니다.
하지만 실제 프로젝트에서는 데이터베이스, 네트워크, 파일 시스템 등 다른 복잡한 모듈에 의존하는 코드를 테스트해야 하는 경우가 훨씬 많습니다.
예를 들어, '사용자 정보를 데이터베이스에서 가져와 이메일을 보내는' 기능을 테스트한다고 상상해봅시다. 이 테스트를 위해 실제 데이터베이스를 연결하고 실제 이메일을 발송해야 할까요? 그랬다간 테스트는 매우 느려지고, 네트워크나 DB 상태에 따라 성공/실패가 달라지며, 테스트할 때마다 스팸 메일이 발송되는 끔찍한 상황이 벌어질 겁니다.
이럴 때 필요한 것이 바로 '모의 객체(Mock Object)'입니다.
모의 객체는 실제 객체인 척하는 '가짜' 객체입니다. 실제 DB에 접속하는 대신 "DB에서 사용자 정보를 달라고 하면, '홍길동'이라는 가짜 정보를 줄게"라고 미리 설정해둘 수 있습니다. 이메일 발송기 대신 "이메일을 보내라는 요청이 오면, 그냥 '요청 받음'이라고 기록만 할게"라고 행동하는 가짜 객체를 만드는 거죠.
Google Mock은 이런 모의 객체를 쉽게 만들고 관리할 수 있도록 도와주는 프레임워크입니다. Google Test와 같은 저장소에 포함되어 있어 함께 사용하기 매우 편리합니다.
Google Mock을 사용하는 것은 내용이 방대하여 이 글에서 모두 다루기는 어렵지만, 어떤 느낌인지만 살짝 보여드릴게요.
// 가상 인터페이스
class Database {
public:
virtual ~Database() = default;
virtual std::string getUserEmail(int userId) = 0;
};
// Google Mock으로 만든 가짜 Database
class MockDatabase : public Database {
public:
MOCK_METHOD(std::string, getUserEmail, (int userId), (override));
};
// 테스트 코드
TEST(EmailServiceTest, SendsEmailToUser) {
MockDatabase mock_db;
// 기대 행위 설정:
// userId가 123으로 getUserEmail이 호출되면,
// "test@example.com"을 반환하도록 설정
EXPECT_CALL(mock_db, getUserEmail(123))
.WillOnce(::testing::Return("test@example.com"));
// EmailService에 실제 DB 대신 가짜 DB(mock_db)를 주입
EmailService service(&mock_db);
// 기능 실행
bool result = service.sendWelcomeEmail(123);
// 결과 검증
EXPECT_TRUE(result);
}
위 코드에서는 실제 DB 연결 없이도 `EmailService`가 `Database`와 올바르게 상호작용하는지를 완벽하게 테스트할 수 있습니다. 이처럼 모의 객체를 활용하면 테스트 대상을 외부 의존성으로부터 완벽하게 분리하여, 오직 해당 모듈의 로직에만 집중하여 테스트할 수 있게 됩니다.
이러한 고급 기능들은 여러분의 테스트 코드를 더욱 강력하고 유연하게 만들어 줄 겁니다. 특히 복잡한 레거시 시스템을 다루거나, 외부 시스템과의 연동이 많은 프로젝트를 진행한다면 파라미터화 테스트와 모의 객체는 여러분의 개발 생산성을 극적으로 향상시켜 줄 필살기가 될 것입니다. 이런 고급 스킬들을 잘 익혀두면, 재능넷 같은 플랫폼에서 여러분의 전문성을 어필할 때도 큰 도움이 될 거예요! 😉
6장: 좋은 테스트란 무엇일까? TDD 철학과 실전 꿀팁 🍯
이제 C++에서 Google Test를 사용하는 기술적인 방법은 대부분 익혔습니다. 하지만 도구를 잘 다루는 것과, 그 도구로 훌륭한 작품을 만드는 것은 다른 이야기입니다. 마지막으로, 기술을 넘어 '좋은 테스트'를 작성하기 위한 철학과 실전에서 유용한 몇 가지 팁들을 공유하고자 합니다.
6.1. 좋은 테스트의 조건: F.I.R.S.T. 원칙
어떤 테스트 코드가 '좋은 테스트'일까요? 업계에서는 흔히 F.I.R.S.T. 라는 다섯 가지 원칙을 이야기합니다. 여러분의 테스트 코드가 이 원칙들을 잘 지키고 있는지 항상 되돌아보는 습관을 들여보세요.
- Fast (빠르게): 테스트는 빨라야 합니다. 테스트 실행이 오래 걸리면 개발자들은 테스트를 자주 실행하는 것을 꺼리게 되고, TDD의 빠른 피드백 사이클이 깨지게 됩니다. 느린 테스트는 개발 흐름을 방해하는 주범입니다.
- Independent/Isolated (독립적으로): 각 테스트는 서로에게 의존해서는 안 됩니다. 어떤 테스트가 다른 테스트의 결과에 영향을 미치거나, 특정 순서로 실행되어야만 성공한다면 그것은 잘못된 테스트입니다. 테스트는 어떤 순서로 실행되든, 혹은 단독으로 실행되든 항상 동일한 결과를 내야 합니다. (테스트 픽스처가 이 원칙을 지키는 데 큰 도움을 주죠!)
- Repeatable (반복 가능하게): 테스트는 어떤 환경(내 노트북, 동료의 컴퓨터, 빌드 서버 등)에서도 항상 동일한 결과를 반환해야 합니다. 현재 시간, 네트워크 상태, 파일 시스템의 특정 파일 존재 여부 등 외부 환경에 의존하는 테스트는 반복 가능성을 해치는 주된 요인입니다. (모의 객체가 이런 외부 의존성을 제거하는 데 효과적입니다.)
- Self-Validating (스스로 검증 가능하게): 테스트는 실행 결과가 성공인지 실패인지 명확하게 스스로 말해주어야 합니다. 테스트를 실행한 후, 사람이 직접 로그 파일을 열어보거나 화면에 출력된 값을 눈으로 확인해야 한다면 그것은 자동화된 테스트가 아닙니다. `EXPECT_*`, `ASSERT_*` 같은 어설션이 바로 이 원칙을 지키기 위한 도구입니다.
- Timely/Thorough (시기적절하고 철저하게): '시기적절하게'는 TDD의 관점에서 테스트를 기능 코드보다 먼저, 혹은 거의 동시에 작성해야 함을 의미합니다. '철저하게'는 테스트가 충분한 케이스를 커버해야 함을 의미합니다. 정상적인 케이스뿐만 아니라, 경계값(boundary values), 예외적인 상황 등 중요한 엣지 케이스들을 놓치지 않고 테스트해야 합니다.
F.I.R.S.T. 원칙을 기억하세요! 이 원칙들은 단순히 테스트 코드를 위한 것이 아니라, 결국 유지보수하기 좋고 안정적인 소프트웨어를 만들기 위한 지침입니다. 여러분의 테스트 스위트가 이 원칙들을 잘 따르고 있다면, 여러분은 코드 변경에 대한 두려움 없이 언제나 자신감 있게 리팩토링하고 새로운 기능을 추가할 수 있을 겁니다.
6.2. 무엇을 테스트하고, 무엇을 테스트하지 말아야 할까?
모든 코드를 100% 테스트하는 것이 항상 최선은 아닙니다. 테스트 코드를 작성하고 유지보수하는 데에도 비용이 들기 때문이죠. 우리는 한정된 자원으로 최대의 효과를 내야 합니다. 즉, '중요한 것'에 집중해야 합니다.
👍 테스트해야 할 것들:
- 비즈니스 로직, 알고리즘: 애플리케이션의 핵심적인 로직과 계산 과정은 최우선 테스트 대상입니다.
- 경계 조건 (Boundary Conditions): `0`, `-1`, `null`, 빈 문자열, 최대값 등 경계에 있는 값들을 처리하는 로직은 버그가 발생하기 쉬운 곳입니다.
- 예외 처리 경로: 에러가 발생했을 때 프로그램이 정상적으로 대처하는지 확인하는 것은 매우 중요합니다.
- 복잡한 조건문(if/else, switch) 분기: 모든 분기가 의도대로 동작하는지 확인해야 합니다.
👎 테스트할 필요가 없거나, 피해야 할 것들:
- 아주 단순한 Getter/Setter: 내부 변수 값을 그대로 반환하거나 설정만 하는 메소드는 테스트할 가치가 거의 없습니다. (물론 Getter/Setter 안에 로직이 있다면 이야기가 달라집니다.)
- 외부 라이브러리/프레임워크 자체의 기능: 우리가 사용하는 표준 라이브러리나 외부 프레임워크가 올바르게 동작하는지는 우리가 테스트할 영역이 아닙니다. 그건 그 라이브러리를 만든 사람들이 이미 테스트했을 겁니다. 우리는 그 라이브러리를 '우리가 올바르게 사용하고 있는지'를 테스트해야 합니다.
- 구현 세부사항에 너무 의존적인 테스트: 테스트는 코드의 '무엇을(what)' 하는지에 집중해야지, '어떻게(how)' 하는지에 너무 얽매이면 안 됩니다. 예를 들어, 어떤 메소드가 내부적으로 `std::vector`를 쓰든 `std::list`를 쓰든, 그 결과만 같다면 테스트는 통과해야 합니다. 구현 방식이 바뀔 때마다 테스트가 깨진다면, 리팩토링을 방해하는 나쁜 테스트가 됩니다.
6.3. 테스트 이름, 예술처럼 짓기
테스트 케이스의 이름은 그 자체로 하나의 훌륭한 '문서'가 될 수 있습니다. 잘 지은 테스트 이름만 봐도 이 테스트가 무엇을 검증하는지, 어떤 상황을 가정하는지 명확하게 알 수 있어야 합니다.
`TEST(MyTest, Test1)` 같은 이름은 최악입니다. 😱
다음과 같은 패턴을 사용해 보세요.
`TEST_F(테스트대상_클래스명, 메소드명_상황_기대행동)`
예를 들어,
- `TEST_F(BankAccountTest, Withdraw_InsufficientFunds_ThrowsException)`
(BankAccount의 Withdraw 메소드는, 잔액이 부족한 상황에서, 예외를 던진다.) - `TEST_F(StringCalculatorTest, Add_CustomDelimiter_ReturnsSum)`
(StringCalculator의 Add 메소드는, 커스텀 구분자가 주어졌을 때, 합계를 반환한다.) - `TEST_F(LoginServiceTest, Login_InvalidPassword_ReturnsFalse)`
(LoginService의 Login 메소드는, 비밀번호가 틀린 경우, false를 반환한다.)
어떤가요? 테스트 이름만 읽어도 기능 명세서의 한 줄을 읽는 것 같지 않나요? 이렇게 명확한 이름은 테스트가 실패했을 때 원인을 파악하는 시간을 극적으로 줄여주고, 다른 개발자가 코드를 이해하는 데에도 큰 도움을 줍니다.
TDD와 유닛 테스트는 단순히 버그를 찾는 기술이 아닙니다. 그것은 더 나은 설계를 유도하고, 코드에 대한 자신감을 심어주며, 동료와의 협업을 원활하게 하는 개발 문화이자 철학입니다. 오늘 배운 내용들을 꾸준히 실천한다면, 여러분의 개발 라이프는 분명 더 안정적이고 즐거워질 것입니다.
마치며: 이제 당신도 테스트 장인! 자신감 있는 코딩을 향하여 🏁
정말 긴 여정이었습니다! 우리는 왜 테스트가 필요한지에 대한 이야기부터 시작해서, Google Test 환경 설정, 핵심 어설션, 테스트 픽스처, 그리고 TDD 사이클을 직접 돌려보는 실습까지 함께했습니다. 나아가 파라미터화 테스트나 죽음 테스트 같은 고급 기능과 좋은 테스트를 작성하기 위한 원칙들까지 살펴보았죠.
이 모든 지식의 핵심은 단 하나, '자신감'입니다.
잘 짜인 테스트 스위트는 여러분의 코드를 24시간 지켜주는 든든한 보초병과 같습니다. 이 보초병이 있기에 우리는 과감하게 코드를 리팩토링할 수 있고, 새로운 기능을 두려움 없이 추가할 수 있습니다. "이거 고치면 다른 게 터지는 거 아냐?"라는 불안감 대신, "테스트만 통과하면 문제없어!"라는 확신을 가지고 개발에 임할 수 있게 되는 것이죠.
물론, 오늘 배운 모든 것을 당장 모든 프로젝트에 적용하기는 어려울 수 있습니다. 특히 거대한 레거시 코드베이스에 테스트를 추가하는 것은 큰 도전일 수 있죠.
하지만 처음부터 완벽할 필요는 없습니다. 오늘 새로 작성하는 작은 클래스 하나부터 테스트를 추가해보세요. 버그를 수정했다면, 그 버그가 재발하지 않도록 보장하는 테스트 케이스를 하나 추가해보세요. 그렇게 작은 성공을 하나씩 쌓아가다 보면, 어느새 테스트를 작성하지 않은 코드가 더 어색하게 느껴지는 자신을 발견하게 될 겁니다.
테스트는 더 이상 '하면 좋은 것'이 아니라, 프로페셔널 소프트웨어 개발의 '필수' 요소가 되었습니다. 오늘 배운 지식들이 여러분의 C++ 개발 여정에 든든한 무기가 되기를 바랍니다. 이제 두려워 말고, 자신감 있게 코딩하세요! 여러분의 뒤에는 수많은 테스트들이 버티고 서 있을 테니까요. 🚀
댓글 작성
이 글에 대한 여러분의 생각을 들려주세요
로그인이 필요합니다
댓글을 작성하려면 먼저 로그인해주세요.
댓글 0
지식인의 숲 - 지적 재산권 보호 고지
지적 재산권 보호 고지
- 저작권 및 소유권: 본 컨텐츠는 재능넷의 독점 AI 기술로 생성되었으며, 대한민국 저작권법 및 국제 저작권 협약에 의해 보호됩니다.
- AI 생성 컨텐츠의 법적 지위: 본 AI 생성 컨텐츠는 재능넷의 지적 창작물로 인정되며, 관련 법규에 따라 저작권 보호를 받습니다.
- 사용 제한: 재능넷의 명시적 서면 동의 없이 본 컨텐츠를 복제, 수정, 배포, 또는 상업적으로 활용하는 행위는 엄격히 금지됩니다.
- 데이터 수집 금지: 본 컨텐츠에 대한 무단 스크래핑, 크롤링, 및 자동화된 데이터 수집은 법적 제재의 대상이 됩니다.
- AI 학습 제한: 재능넷의 AI 생성 컨텐츠를 타 AI 모델 학습에 무단 사용하는 행위는 금지되며, 이는 지적 재산권 침해로 간주됩니다.
아직 댓글이 없습니다.