들어가며
올해 개인 목표중 하나였던 Unit Test 학습에 대해 달성했다.
○ Unit Test 역량 강화
☑ "테스트 주도 개발" 학습.
☑ Qt Coco 세미나 참석.
☑ 현재 개발중인 프로젝트에 부분 도입
제일 먼저 "테스트 주도 개발"을 교재로 학습하면서 유닛 테스트에 대한 개념을 잡고 간단한 기능을 구현하며 연습했다.
테스트 툴은 제일 유명하고 다루기 관리가 쉬운 google의 gtest를 선정했다.
유닛테스트에 대해 부정적인 느낌으로 학습을 시작했지만, 장단점을 알고 Qt 세미나에 참석해 Coco 코드 커버리지 툴까지 접한 후에는 생각이 많이 달라졌다. 현재 프로젝트에 적용하며 "코드안정성 상승"과 "개발속도 상승" 이라는 두마리 토끼를 잡았으며 앞으로도 계속 사용할 생각이다.
TDD에서는 극단적으로 테스트 위주로 개발을 진행하는데 나는 이렇게까지 긍정적으로 평가하지는 않는다.
현재 진행중인 프로젝트가 처음부터 유닛테스트가 고려되지 않은 환경인 점도 큰 영향을 끼치고,
명확한 목적이 있을때, 일정부분에 대해서 유닛 테스트를 개발하는 게 맞는 듯 하다.
유닛 테스트 도입 이유
유닛테스트, TDD, googletest에 대한 관심은 가지고 있었다. 전직장에는 영상처리 업무를 담당할때는 매번 변화하는 이미지 처리에 관한 새로운 알고리즘 개발이 필요했기에 굳이 필요성을 느끼지 못했다.
직장을 옮기며 투입된 프로젝트에서 C++언어를 사용해 메모리 변환, 산술계산 처리를 하는 라이브러리 개발을 담당하게 됐다.
예측되는 여러 장점들에 라이브러리 개발에 유닛테스트를 도입하기로 결정했다.
- 라이브러리 개발의 안정성을 잡을 수 있다.
- 구현해야하는 기능에 대한 Test케이스(입력값과 예측값의 테이블)가 있어 테스트케이스를 만드는 소요가 적다.
- 유닛 테스트 코드를 통해 팀원들과 테스트 이력을 공유할 수 있다.
- 라이브러리 기능 변경시 테스트 자동화로 업무속도가 향상된다.
- 라이브러리라 프로젝트 의존성이 없으며, 유닛테스트 환경 구성 및 배포가 쉽다.
프로젝트에 첫 도입
우리 프로젝트는 mingw 컴파일러를 이용하기에 gtest를 다운받아 컴파일하고, 불필요한 라이브러리를 제거하고 thirdParty에 추가하면서 배포과정이 끝난다.
보통 컴파일은 개발자들이 각자 PC에서 하고 소스코드를 업로드하지만, 컴파일러가 정해져있기때문에 라이브러리를 빌드해서 고정으로 thirdParty에 박아버린다.
PS C:\Minerva\branches\Core\googletest> tree /A
C:.
+---include
| \---gtest
| \---internal
| \---custom
\---lib
유닛테스트 진행
일단 돌아가도록 CMake를 통해 googletest경로를 잡아주고, 내가 만든 라이브러리를 복사해서 컴파일 환경을 구성했다.
그 후 불필요한 내용은 지우고, 가독성이 좋게 정리하다보면 나름 괜찮은 CMakeLists.txt 파일이 된다.
cmake_minimum_required(VERSION 3.14) # Qt5 find_package는 더 높은 버전을 권장합니다.
project(UnitTests)
file(GLOB TEST_SOURCES
"${CMAKE_CURRENT_SOURCE_DIR}/*_test.cpp"
)
add_executable(UnitTests ${TEST_SOURCES})
set_target_properties(UnitTests PROPERTIES WIN32_EXECUTABLE OFF)
# gtest 타겟(gtest, gtest_main)을 로드합니다.
include("${CMAKE_CURRENT_SOURCE_DIR}/../../googletest/googletest.cmake")
# Modern CMake 방식으로 include 경로를 설정합니다.
target_include_directories(UnitTests PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/../" # Core 라이브러리 헤더 경로
)
# Modern CMake 방식으로 라이브러리 경로를 설정합니다.
target_link_directories(UnitTests PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/../bin/i686-w64-mingw32_8.1.0"
)
# 라이브러리를 연결합니다.
target_link_libraries(UnitTests PRIVATE
$<$<CONFIG:Debug>:Cored>
$<$<CONFIG:Release>:Core>
# gtest 라이브러리 링크
gtest
gtest_main
)
# 테스트 실행 시 libCore.dll을 찾을 수 있도록 빌드 디렉터리로 복사
add_custom_command(TARGET UnitTests POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${CMAKE_CURRENT_SOURCE_DIR}/../bin/i686-w64-mingw32_8.1.0/libCore$<$<CONFIG:Debug>:d>.dll"
$<TARGET_FILE_DIR:UnitTests>
)
# GTest 자동 실행 등록
enable_testing()
add_test(NAME UnitTests COMMAND UnitTests)
그리고 테스트 코드를 추가해 테스트가 진행되도록 만들고, 라이브러리의 작동상태와 기대값과 결과값을 확인한다.
현재 테스트중인 프로젝트는 전처리 과정을 통해 C++ 언어로 변환하는 과정을 거친다.
이때 실제로 생성될 수 있는 코드를 가지고 테스트해서 불필요한 테스트를 줄이고, 필요한 테스트를 필터링할 수 있었다.
#include "Source/DirectVariable.h"
#include "Source/Fc/BitWise.h"
#include <gtest/gtest.h>
using namespace MvxCore;
class BitWiseTest : public ::testing::Test {
protected:
void TearDown() override {
// 각 테스트 후에 실행되는 코드
CLEAR_DIRECT_VAR_I();
CLEAR_DIRECT_VAR_M();
CLEAR_DIRECT_VAR_Q();
}
};
TEST_F(BitWiseTest, AND) {
EXPECT_EQ(AND(255, 10), 10);
EXPECT_EQ(AND(0x0F0F, 0x3333), 0x0303);
EXPECT_EQ(AND(IB(0).ref(), 0xFF), 0x00);
IB(0) = 0xAA;
EXPECT_EQ(AND(IB(0).ref(), 0x0F), 0x0A);
EXPECT_EQ(AND(IX(0, 0), MvxCore::True), MvxCore::False);
EXPECT_EQ(AND(IX(0, 0), MvxCore::False), MvxCore::False);
IX(0, 0) = MvxCore::True;
EXPECT_EQ(AND(IX(0, 0), MvxCore::True), MvxCore::True);
IB(10) = AND(0xFF, 0xAA);
EXPECT_EQ(IB(10).ref(), static_cast<BYTE>(0xAA));
IX(10, 0) = AND(MvxCore::True, MvxCore::True);
EXPECT_EQ(IX(10, 0), MvxCore::True);
// N개 인자 테스트
EXPECT_EQ(AND(0xFF, 0x0F, 0x33), 0x03);
EXPECT_EQ(AND(0xFF, 0x0F, 0x33, 0xCC), 0x00);
EXPECT_EQ(AND(0xFF, 0x0F, 0x33, 0x02, 0xAA), 0x02);
}
위에 있는 BitWiseTest의 TearDown()은 테스트케이스가 끝날때마다 실행되는 함수로, 내 프로젝트에서 사용하는 메모리를 청소하는 기능을 실행해, 각 테스트가 독립적으로 실행되도록 추가한 코드이다.
독립된 테스트의 중요성은 "테스트 주도 개발" 책에서 배울 수 있었다.
유닛테스트를 빌드 후 돌리면 기대값과 결과값이 다른 함수나, 성공여부를 통해 라이브러리가 잘 동작하는지 검수할 수 있었다.
[----------] 9 tests from BitWiseTest
[ RUN ] BitWiseTest.AND
[ OK ] BitWiseTest.AND (1 ms)
C:\Minerva\branches\Core\MinervaX_Core\tests\BitShift_test.cpp:113: Failure
Expected equality of these values:
result4
Which is: 0
0b1010110000000000
Which is: 44032
회고.
마침 유닛테스트를 적용하기 최적의 개발소요가 있었고, 환경설정부터 자동화까지 해볼 수 있는 환경이 최적의 조건이라 생각된다.
이번을 시작으로 ThirdParty를 추가할 수 있었고, 이어서 개발한 Gateway Server & Client 개발에 테스트코드를 작성하며 빠르게 개발할 수 있었다.
무엇보다 Unittest를 도입하기 전엔 어떤 테스트를 진행했고 누락했는지를 기록하는 방법이 없었는데,
이제는 코드를 통해 누가 어떤 테스트를 진행했는지 SVN에 업로드해 관리할 수 있게 됐다. 다음 개발자는 내가 작성한 환경과 테스트코드를 통해 더 쉽고 즐겁게 일하는걸 기대해 본다.
'개발' 카테고리의 다른 글
| Service 기반 시스템 권한 기능 구현기 — 권한 분리 환경에서 IPC구현과 QLocalSocket DACL 문제 (0) | 2025.12.12 |
|---|---|
| Service 기반 시스템 권한 기능 구현기 — JSON 프로토콜과 OS 추상화 설계 (1) | 2025.12.10 |
| Windows·Linux에서 시스템 권한을 다루는 올바른 아키텍처 설계 (1) | 2025.12.03 |
| 산업용 제어 장치에서의 인증서 처리와 OPC UA 보안 이해하기 (1) | 2025.11.28 |
| googletest(gtest) 설치부터 빌드까지: C++11 개발 환경 구성기 (1) | 2025.11.25 |