Overview
c/c++ 개발자라면 unit test 작성할 때 cunit, cppunit, gtest 중에 하나를 사용해봤을 것이다. cunit에 대해서는 굳지 설명하지 않겠다. cppunit 과 gtest를 비교하자면 cppunit은 테스트케이스를 작성할 때 개발자가 코딩해줘야 하는 부분이 좀 더 많다.
예를 들면, 테스트결과를 XML 파일로 생성하려면 cppunit인 경우에는 XmlOutputter 클래스를 이용하여 테스트 결과과 XML 파일로 생성하도록 직접 코딩해야 한다. 그런데 gtest인 경우는 단지 테스트 Application을 실행할 때 실행 파라미터에 "--gtest_output=xml:result.xml" 을 사용하기만 하면 된다.
설치
- 환경: ubuntu 16.04 (64bit)
- gtest package 다운로드
- my account $> sudo apt install libgtest-dev
- Above installation does not include binary library but only gtest source code.
- Therefore, you should compile it.
- libgtest-dev is installed as source code at /usr/src/gtest
- gtest 소스파일을 빌드하여 library 생성
- /usr/src/gtest $> sudo cmake -DBUILD_SHARED_LIBS=1 .
- /usr/src/gtest $> sudo make
- /usr/src/gtest $> sudo make install
- or /usr/src/gtest $> sudo cp libgtest.so libgtest_main.so /usr/lib
- for static library, execute cmake command without -DBUILD_SHARED_LIBS=1
- for 32bit library, use -m32 option as below.
- sudo cmake -DCMAKE_CXX_FLAGS=-m32
첫번째 샘플
아래 샘플은 int sum(int a, int b) 이라는 함수를, gtest를 사용하여 호출 결과를 검증하는 예제이다.
TEST( <TestCase Name>, <Test Name>) 형태로 작성하고 body { } 안에는 그냥 보통의 함수 body를 작성하듯이 작성하면 된다. 변수를 선언할 수 도 있고 c/c++ 제어문 및 함수호출도 가능하다. 다만 테스트 대상에 대해서 ASSERT_* 또는 EXPECT_* 를 사용하여 값을 검증하도록 한다.
main 함수안에는 ::testing::InitGoogleTest(&argc, argv)를 호출한 후 RUN_ALL_TESTS() 호출하는 코드가 전부이다.
즉, 개발자는 TEST를 이용하여 테스트케이스 작성에만 집중하면 된다. 나머지는 gtest가 알아서 모두 처리해준다. CppUnit을 사용해 본 개발자라면 gtest가 얼마나 심플한지 알 것이다.
테스트결과를 XML 파일로 출력하려면 테스트 Application을 실행할 때 --gtest_output=xml:result.xml 파라미터를 사용하여 실행하면 된다. 이 파라미터 정보는 main 함수안에 InitGoogleTest 함수 호출시 파라미터로 전달된다. 그 이후는 gtest에서 알아서 처리된다.
와우~!
TEST( <TestCase Name>, <Test Name>) 형태로 작성하고 body { } 안에는 그냥 보통의 함수 body를 작성하듯이 작성하면 된다. 변수를 선언할 수 도 있고 c/c++ 제어문 및 함수호출도 가능하다. 다만 테스트 대상에 대해서 ASSERT_* 또는 EXPECT_* 를 사용하여 값을 검증하도록 한다.
main 함수안에는 ::testing::InitGoogleTest(&argc, argv)를 호출한 후 RUN_ALL_TESTS() 호출하는 코드가 전부이다.
즉, 개발자는 TEST를 이용하여 테스트케이스 작성에만 집중하면 된다. 나머지는 gtest가 알아서 모두 처리해준다. CppUnit을 사용해 본 개발자라면 gtest가 얼마나 심플한지 알 것이다.
테스트결과를 XML 파일로 출력하려면 테스트 Application을 실행할 때 --gtest_output=xml:result.xml 파라미터를 사용하여 실행하면 된다. 이 파라미터 정보는 main 함수안에 InitGoogleTest 함수 호출시 파라미터로 전달된다. 그 이후는 gtest에서 알아서 처리된다.
와우~!
hellogtest(main.cpp)
#include <gtest/gtest.h> int sum(int a, int b) { return (a + b); } TEST (MyTestCase1, PositiveTest) { EXPECT_EQ (3 , sum(1, 2)); EXPECT_EQ (5 , sum(2, 3)); EXPECT_EQ (300 , sum(100, 200)); } TEST (MyTestCase2, NegativeTest) { EXPECT_EQ (-3 , sum(-1, -2)); EXPECT_EQ (-5 , sum(-2, -3)); EXPECT_EQ (-300 , sum(-100, -200)); } int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); }
CMakeLists.txt
CMakeLists.txt 파일은 위 샘플 코드를 빌드하기 위해 Makefile 을 생성하기 위한 cmake script 파일이다. 아래처럼 작성하면 되는데 gtest는 c++11 가 필요하다. -std=c++11 를 추가하도록 한다(libgtest.so 파일이 /usr/lib 에 위치해야 함).
cmake_minimum_required(VERSION 2.8) project(hello_gtest) add_definitions( -std=c++11) add_executable(hellogtest main.cpp) target_link_libraries(hellogtest gtest)
실행결과(Without any parameters)
위 샘플은 MyTestCase1, MyTestCase2 라는 2개의 테스트 케이스가 있고 테스트 케이스 안에는 각각 PositiveTest, NegativeTest 라는 테스트가 각각 1개 씩 총 2개의 테스트가 있다.
이 샘플을 돌렸을 때 테스트 결과는 아래처럼 출력된다.
[==========] Running 2 tests from 2 test cases. [----------] Global test environment set-up. [----------] 1 test from MyTestCase1 [ RUN ] MyTestCase1.PositiveTest [ OK ] MyTestCase1.PositiveTest (0 ms) [----------] 1 test from MyTestCase1 (0 ms total) [----------] 1 test from MyTestCase2 [ RUN ] MyTestCase2.NegativeTest [ OK ] MyTestCase2.NegativeTest (0 ms) [----------] 1 test from MyTestCase2 (0 ms total) [----------] Global test environment tear-down [==========] 2 tests from 2 test cases ran. (0 ms total) [ PASSED ] 2 tests.
XML 출력 결과
다음과 같이 실행하면 아래와 같은 출력 결과를 얻을 수 있다.- hellogtest --gtest_output=xml:result.xml
result.xml
<?xml version="1.0" encoding="UTF-8"?> <testsuites tests="2" failures="0" disabled="0" errors="0" timestamp="2017-12-14T10:29:59" time="0" name="AllTests"> <testsuite name="MyTestCase1" tests="1" failures="0" disabled="0" errors="0" time="0"> <testcase name="PositiveTest" status="run" time="0" classname="MyTestCase1" /> </testsuite> <testsuite name="MyTestCase2" tests="1" failures="0" disabled="0" errors="0" time="0"> <testcase name="NegativeTest" status="run" time="0" classname="MyTestCase2" /> </testsuite> </testsuites>
Test Fixture
어떠한 기능 테스트을 할 때 아래처럼 하는 경우가 많다.- [setup step] 테스트를 수행할 수 있는 환경을 만든다.
- [테스트 step] 테스트를 수행한다.
- [검증 step] 테스트 결과를 검증한다.
- [teardown step] 테스트 환경을 모두 close/relse 한다.
Fixture를 사용하는 경우, setup/teardown을 통하여 테스트 환경이 매번 초기화할 수 있기 때문에 반복 테스트가 가능하다. 그리고 시나리오 테스트처럼 어떠한 선행 조건하에서 테스트할 때 매우 유용하게 사용할 수 있는 기능이다.
두번째 샘플(Test Fixture)
Fixture를 사용하려면 ::testing::Test 클래스를 상속받는 사용자 클래스를 구현하야 한다. 이 클래스에는 void SetUp(), void TearDown() 함수를 overriding 해야 하는데 SetUp, TearDown 이 함수들은 테스트 케이스가 실행 전후 마다 실행될 것 이다. 그리고 위 예제와 다른 부분이 "TEST" 대신에 "TEST_F" 가 사용되었다. 여기서 "F"는 Fixture를 의미한다.
#include <gtest/gtest.h> class MyTestFixture : public ::testing::Test { public: MyTestFixture() { } void SetUp() { printf("[%s:%d]\n", __FUNCTION__, __LINE__); } void TearDown() { printf("[%s:%d]\n", __FUNCTION__, __LINE__); } int sum(int a, int b) { return (a + b); } }; TEST_F(MyTestFixture, TestSum) { ASSERT_GT ( 10 , sum( 1, 2)); } int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); }
실행결과
아래처럼 SetUp, TearDown 함수가 호출된 것을 알 수 있다. 아래 출력 내용에는 보이지 않지만 sum 함수는 SetUp, TearDown 호출 사이에서 호출되었다.
[==========] Running 1 test from 1 test case. [----------] Global test environment set-up. [----------] 1 test from MyTestFixture [ RUN ] MyTestFixture.TestSum [SetUp:12] [TearDown:17] [ OK ] MyTestFixture.TestSum (0 ms) [----------] 1 test from MyTestFixture (0 ms total) [----------] Global test environment tear-down [==========] 1 test from 1 test case ran. (0 ms total) [ PASSED ] 1 test.
ASSERT_* vs EXPECT_*
테스트 결과를 검증하는 함수로 ASSERT_* 와 EXPECT_* 두 종류가 있다. 아래는 gtest document에서 발췌한 내용인데 이 두 종류 함수의 차이는 ASSERT_* 함수는 실패시 해당 테스트 케이스를 중단한다. 반면 EXPECT_* 함수는 해당 테스트는 실패하지만 그 다음 테스트는 계속 진행한다. 따라서 테스트 용도에 맞게 ASSERT_* 와 EXPECT_* 사용해야 한다.
ASSERT_*
versions generate fatal failures when they fail, and abort the current function.EXPECT_*
versions generate nonfatal failures, which don't abort the current function. UsuallyEXPECT_*
are preferred, as they allow more than one failures to be reported in a test. However, you should useASSERT_*
if it doesn't make sense to continue when the assertion in question fails.
Global Set-Up and Tear-Down
Fixture의 setup/teardown은 테스트 케이스 마다 매번 실행된다. 그런데 글로벌하게 한번 setup/teardown 하는 기능이 필요할 수 있다.아래 예제는 첫번째 샘플을 이용하였다. ::testing::Environment 클래스를 상속받는 사용자 클래스를 작성해야 하는데 void SetUp(), void TearDown() 함수를 overriding 해야 한다. 그리고 main 함수에서 ::testing::AddGlobalTestEnvironment() 함수를 호출하여 등록하면 된다.
#include <gtest/gtest.h> int sum(int a, int b) { return (a + b); } TEST (MyTestCase1, PositiveTest) { EXPECT_EQ (3 , sum(1, 2)); EXPECT_EQ (5 , sum(2, 3)); EXPECT_EQ (300 , sum(100, 200)); } TEST (MyTestCase2, NegativeTest) { EXPECT_EQ (-3 , sum(-1, -2)); EXPECT_EQ (-5 , sum(-2, -3)); EXPECT_EQ (-300 , sum(-100, -200)); } class GlobalEnv : public ::testing::Environment { public: GlobalEnv(){} ~GlobalEnv(){} void SetUp() { printf("GlobalEnv::SetUp\n"); } void TearDown() { printf("GlobalEnv::TearDown\n"); } }; int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); ::testing::AddGlobalTestEnvironment(new GlobalEnv); return RUN_ALL_TESTS(); }
테스트 결과
GlobalEnv::SetUp, TearDown이 아래처럼 1회 호출된다.
[==========] Running 2 tests from 2 test cases. [----------] Global test environment set-up. GlobalEnv::SetUp [----------] 1 test from MyTestCase1 [ RUN ] MyTestCase1.PositiveTest [ OK ] MyTestCase1.PositiveTest (0 ms) [----------] 1 test from MyTestCase1 (0 ms total) [----------] 1 test from MyTestCase2 [ RUN ] MyTestCase2.NegativeTest [ OK ] MyTestCase2.NegativeTest (0 ms) [----------] 1 test from MyTestCase2 (0 ms total) [----------] Global test environment tear-down GlobalEnv::TearDown [==========] 2 tests from 2 test cases ran. (0 ms total) [ PASSED ] 2 tests.
자주 사용하는 실행 파라미터
- GoogleTest를 사용하여 실행 바이너리를 생성하면 GoogleTest framework에서 자동으로 실행 파라미터를 생성해준다. 아래처럼 실행 파라미터들을 확인할 수 있다.
- 실행파일 --help
- --gtest_repeat=1000
- 하나의 Process에서 지정한 횟수만큼 테스트케이스 들이 반복되어 실행한다. 간헐적으로 발생하는 crash등의 문제를 재현할 때 이 기능을 사용하면 매우 편리하다. 그리고 실행된 Process에서 동작하기 때문에 테스트케이스들이 반복하면서 메모리 릭 증가 상태를 확인하기 용이하다. 와우!
- --gtest_output="xml:result.xml"
- 테스트결과를 xml 형태로 저장
- --gtest_break_on_failure
- 보통 ASSERT_EQ 같은 구분은 해당 테스트 케이스를 Abort만 시키지만 이 flag를 사용하면 coredump를 생성시켜준다.
- 즉, gdb와 함께 unittest를 실행하여 위 flag를 사용하는 경우, ASSERT_EQ에 걸리는 경우는 해당 위치에 break point를 걸어준다. 와우!
- --gtest_filter=테스트케이스
- 특정 테스트케이스만 실행할 때 유용하다.
- 테스트 케이스 이름에 *, ? 사용 가능하고 여러 테스트 케이스를 지정할 때 : 으로 구분한다.
Etc...
- 일반함수에서 ASSERT_* 를 사용할 때는 반드시 void 형 return type을 사용해야 한다.
- 아래처럼 값을 리턴하게 되면 “void value not ignored as it ought to be.” 식의 컴파일 에러가 발생한다.
=========
int Test(){
ASSERT_TRUE(true);return 0;}
References
- https://en.wikipedia.org/wiki/Google_Test
- https://github.com/google/googletest
- https://en.wikipedia.org/wiki/Test_fixture
댓글
댓글 쓰기