개발/C, C++

C++에 google test를 테스트 프로젝트에 적용해보기

미국산곰고기 2022. 8. 11. 12:03

테스트 구조는 알았고, 사내에 테스트를 적용을 마쳤다.

 

이해한 내용을 정리해서 테스트 프로젝트를 하나 작성하고,

이 아이를 "단위 테스트가 없던 프로젝트" 에서 "단위 테스트가 포함된 프로젝트" 로 바꾸는 과정을 살펴보자

 

CentOS 7

google-test

 

단위 테스트가 없는 상태의 프로젝트와 단위 테스트를 포함한 프로젝트를 각각 첨부한다.

 

코드는 직접 내려받아서 make 하고 make test 를 통해 테스트 바이너리까지 빌드할 수 있도록 만들어두었다

 

github에서 확인하기

https://github.com/BearMett/UnitTestTester

 

GitHub - BearMett/UnitTestTester: 프로젝트의 테스트 코드 적용기

프로젝트의 테스트 코드 적용기. Contribute to BearMett/UnitTestTester development by creating an account on GitHub.

github.com

 

파일로 내려받기 (tar xf ~)

project_sum_no_test.tar
0.01MB
project_sum_with_test.tar
2.13MB

아래는 테스트 코드가 없는 프로젝트 구조이다.

 

 

sum.h

왼쪽 값과 오른쪽 값을 설정하고 수행하면 결과값을 계산하는 클래스이다.

왼쪽 값과 오른쪽 값은 각각 bool 로 설정 여부를 상태로 가진다.

run 기능은 왼쪽 혹은 오른쪽 값이 설정된 적 없이 run이 수행되면 연산하지 않고 false를 반환

양쪽 값이 설정 된 상태로 run을 수행하면 멤버 변수 value_result 에 연산 결과를 저장

 

전체 코드의 가장 큰 차이는 테스트용 코드, google test 라이브러리 겠지만 그 변화를 바로 체감할 수 있는것은 Makefile 이다, 우선 기본 프로젝트의 Makefile이다.

TARGET_EXEC := sum_runner

BUILD_DIR  := ./bin
OBJ_DIR := ./obj
SRC_DIRS := ./src
INC_DIRS := ./include
#빌드 관련 디렉토리 설정

INC_FLAGS := $(addprefix -I,$(INC_DIRS))
#$INC_DIRS에의 라인마다 접두 -I를 붙여서 include 대상 디렉토리로 설정 (-I./include)

LIB_DIRS :=
LDFALGS :=
#library를 사용하는 경우 포함, 현재 필요 없음

CPPFLAGS := $(INC_FLAGS) -MMD -MP
#dependancy (.d) 파일을 생성하기 위한 플래그, 동시에 이전에 만든 $INC_FLAGS를 조립한다

CXX = g++
CXXFLAGS = -g 
#(디버그 플래그)

SRC := src/main.cpp \
src/sum.cpp
#목적 파일을 생성해야 하는 빌드 대상 소스 목록

OBJS = $(addprefix $(OBJ_DIR)/,$(SRC:.cpp=.o))
#목적 파일 생성시, 빌드 대상 소스.cpp -> obj/빌드 대상 소스.o 
DEPS = $(OBJS:.o=.d)
#목적 파일 생성 후, 목적 파일 이름을 obj/빌드 대상 소스.o -> obj/빌드 대상 소스.d

all: $(BUILD_DIR)/$(TARGET_EXEC)
#빌드의 최종 목적인 실행 파일

$(OBJS): $(OBJ_DIR)/%.o: %.cpp
        $(CXX) -c $(CXXFLAGS) $(INC_FLAGS) $< -MD -o $@ $(LDFALGS)
#목적 파일을 생성하려면 cpp 파일들이 필요

$(BUILD_DIR)/$(TARGET_EXEC): $(OBJS)
        $(CXX) $(OBJS) $(INC_FLAGS) -o $@ $(LDFALGS)
#최종 실행 파일을 생성하려면 o 파일들이 필요


.PHONY: clean
#clean 은 목적이 없으므로 최신을 확인할 필요 없음
clean:
        rm -f $(OBJS) $(BUILD_DIR)/$(TARGET_EXEC) $(DEPS)
#생성한 목적 파일(o), 의존 파일(d), 바이너리를 모두 제거

-include $(DEPS)
#생성한 의존 파일을 할 때 참고

이제 테스트 코드가 포함된 프로젝트의 디렉토리 변화를 보자

 

 

이 변화를 받아들인 Makefile을 살펴보자, 추가된 부분만 있다.

TEST_DIR := ./test
#프로젝트에 test 디렉토리를 확인할 수 있다. 
TEST_INC_DIR := $(INC_DIRS) $(TEST_DIR)/include
TEST_INC_FLAGS := $(addprefix -I,$(TEST_INC_DIR))

TEST_CC := $(TEST_DIR)/sum_test.cc
#테스트 코드 목록
#파일이 늘어나면 구분자를 쓴다
#아래는 예시
NO_USE_CC := $(TEST_DIR)/sum_test.cc \ 
$(TEST_DIR)/min_test.cc \
$(TEST_DIR)/max_test.cc
#테스트에 포함할 소스는 이렇게 추가하면 된다
TEST_CC_OBJ := $(TEST_CC:.cc=.o)
TEST_CC_DEPS := $(TEST_CC_OBJ:.o=.d)

#여기부터는 원래 테스트 해야 할 목적 파일이 들어간다
TEST_SRC := src/sum.cpp
#이 목록은 원래 테스트 해야 할 소스 파일을 넣는다
TEST_OBJ :=  $(TEST_SRC:.cpp=.o)
TEST_DEPS = $(TEST_OBJ:.o=.d)


TEST_LIB_DIR := $(TEST_DIR)/lib/
TEST_LIB := -lgtest -lpthread
TEST_TARGET_EXEC := sum_test_run
#테스트에만 필요한 라이브러리인 gtest를 정해주고
#gtest를 빌드할때 당시 pthread 를 포함했으므로 pthread 라이브러리도 포함

test: $(TEST_DIR)/$(TEST_TARGET_EXEC)
#make test 명령에 대한 정의, 테스트 바이너리를 목적으로 한다

#아래는 조립단계

$(TEST_DIR)/$(TEST_TARGET_EXEC): $(TEST_OBJ) $(TEST_CC_OBJ)
        $(CXX) $(TEST_CC_OBJ) $(TEST_OBJ) $(TEST_INC_FLAGS) -L$(TEST_LIB_DIR) $(TEST_LIB) $(LDFALGS) -o $@

$(TEST_OBJ): $(TEST_SRC)
        $(CXX) -c -g $(CXXFLAGS) $(TEST_INC_FLAGS) $< -MD -o $@ $(LDFALGS)

$(TEST_CC_OBJ): $(TEST_CC)
        $(CXX) -c -g $(CXXFLAGS) $(TEST_INC_FLAGS) $< -MD -o $@ $(LDFALGS)

 

테스트 코드가 포함된 프로젝트를 빌드하고 실행해보는 모습이다.

 

make test 시점에 마지막 g++ 명령을 주목해보면 google test를 빌드하기 위한 정적 라이브러리 파일과 헤더 파일을 가져오는 모습과 원본 목적 파일을 가져오는 모습을 확인할 수 있다.

 

테스트에 사용한 코드는 아래와 같다.

 

생성하자마자 아무 값도 설정하지 않았을 때 실패하기를 기대하는 테스트와

정상적으로 왼쪽에 5, 오른쪽에 3을 놓고 덧셈을 했을 때 맞는 값이 나오길 기대하는 테스트 두종류이다.

include "gtest/gtest.h"
#include "sum.h"

TEST(sum_worker_init_false__Test, sum_worker_test)
{
    sum_worker sum_worker_tester;

    EXPECT_FALSE(sum_worker_tester.run());
    EXPECT_EQ(0, sum_worker_tester.getValue());
}

TEST(sum_worker_sum_test, sum_worker_test)
{
    sum_worker sum_worker_tester;

    sum_worker_tester.setLeft(5);
    sum_worker_tester.setRight(3);
    EXPECT_TRUE(sum_worker_tester.run());
    EXPECT_EQ(8, sum_worker_tester.getValue());
}

int main(int argc, char **argv)
{
    ::testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

다음은 google test에서 사용할 수 있는 EXPECT 목록이다

 

EXPECT_ANY_THROW (구문)

 구문 내에 어떤 예외라도 던지는(throw)지 확인

 구문이라고 표시된 경우 대괄호를 사용해도 표현할 수 있음

EXPECT_ANY_THROW({
int a = 1; 
if(a == 1) 
 throw std::runtime_error("not 1");
})

EXPECT_DEATH

EXPECT_DEATH_IF_SUPPORTED

 프로그램 수행 중 SIGSEGV과 같이 시그널로 죽어버리는 상황이 발생하면 통과

 

EXPECT_DEBUG_DEATH

 프로그램 수행 중 DEBUG 빌드 시 동작하는 ASSERT 로 종료되는 상황이 발생하면 통과

 

EXPECT_EXIT

 프로그램 수행 중 정상 종료(exit) 하면 통과

 

EXPECT_FALSE(상태) 

EXPECT_TRUE(상태)

 EXPECT 하는 상태가 동일하면 통과

 

비교 형제들의 사용법은 간단하다.

bash 조건문과 비슷한 문구들

원하는 조건에 맞추어 대상을 쓰면 된다.

 

E, EQ = Equal

GT = Grater than

LT = Less than

GE = Grater than or EQUAL

LE = Less than or EQUAL

NE = Not Equal

 

EXPECT_DOUBLE_EQ (8 바이트 실수1 = 8 바이트 실수2)

EXPECT_FLOAT_EQ (4 바이트 실수1 = 4 바이트 실수2)

EXPECT_EQ (정수 1 = 정수 2)

EXPECT_GE (정수 1 >= 정수 2)

EXPECT_GT (정수 1 > 정수 2)

EXPECT_LE (정수 1 <= 정수 2)

EXPECT_LT (정수 1 < 정수 2)

EXPECT_NE (정수 1 != 정수 2)

 

EXPECT_NEAR(실수1, 실수2, 오차 범위)

두 실수가 허용하는 오차 범위 안쪽으로 있는지 검사

아래와 같이 사용하면 1.2까지만 검사하므로 테스트를 통과한다.
예시: EXPECT_NEAR(1.23, 1.24, 0.0f);

 

EXPECT_NO_FATAL_FAILURE (구문)

구문을 진행하는 동안 프로그램이 죽지 않으면 통과

 

EXPECT_NO_THROW (구문)

구문을 진행하는 동안 예외를 던지지 않으면 통과