GTEST(GoogleTest)

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)
  1. 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
  2. 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에서 알아서 처리된다.
와우~!

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 functionEXPECT_* versions generate nonfatal failures, which don't abort the current function. Usually EXPECT_* are preferred, as they allow more than one failures to be reported in a test. However, you should use ASSERT_* 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

댓글