순서가 좀 바뀐 것 같지만 각각의 구글 테스트 수트를 통합해서 실행하고 리포트를 생성하는 첫 번째 업무는 잘 마무리되었다. 첫 번째 업무를 수행하면서 배운 내용들은 이후에 블로그에 정리해야겠다.
이제 구글 테스트 수트를 수행해보자! 근데 구글 테스트가 뭐지.. 내가 생각해도 어이가 없다.
간단하게 구글 테스트에 대한 구글링을 수행했다. (라임 지렸다..)
구글 테스트는 구글에서 개발된 C++ 테스트 프레임 워크이다. 인터넷에서 라이브러리를 쉽게 구할 수 있다.
ASSERT_%조건% 혹은 EXPECT_%조건%으로 테스트한다. ASSERT는 실패하면 즉시 바로 함수를 빠져나가고 EXPECT는 그냥 계속 진행한다는 차이점이 있다. 기본적으로 테스트를 모두 돌린 후, 결과 확인을 통해 어떤 테스트가 실패했는지를 확인해야 하기 때문에 ASSERT는 안 쓰고 EXPECT를 쓴다.
% 조건% 에는 TRUE, FALSE, EQ, NE, LT, LE, GT, GE을 사용할 수 있으나 보통 EQ을 사용하여 기댓값과 결괏값을 비교한다.
예를 들어 다음과 같은 파일을 테스트한다고 하자.
sum.h
int sum(int a, int b);
sum.c
#include "sum.h"
int sum(int a, int b){ return a + b; }
아래와 같이 테스트 수트를 생성할 수 있다.
직감적으로 알 수 있지만 TEST(%test suite name%, %test case name%)이 하나의 테스트 케이스 단위가 된다.
UT_sum.cpp
#include <gtest/gtest.h>
extern "C"
{
#include "sum.c"
}
TEST(SumTest, PositiveTest)
{
EXPECT_EQ(2, sum(1,1));
}
TEST(SumTest, NegativeTest)
{
EXPECT_EQ(-2, sum(-1,-1));
}
int main(int argc, char **argv)
{
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
문제는 해당 유닛이 외부 인터페이스를 사용하는 경우다. 만약 위 코드가 다음과 같이 바뀐다면?
interface.h
int ext_sum(int a, int b);
void ext_sum2();
sum.h
int sum(int a, int b);
sum.c
#include "interface.h"
#include "sum.h"
int sum(int a, int b)
{
ext_sum2();
return ext_sum(a, b);
}
이 때 구글 mock 함수가 사용된다. 유니에서 사용되는 외부 인터페이스를 제어하기 위해서. mock의 사용법은 너무 방대해서 다 말하기보단 쓰면서 아래에 정리해야겠다. 위와 같이 바뀐 코드를 테스트하기 위해 업데이트된 테스트 코드는 다음과 같다.
UT_sum.cpp
#include <gtest/gtest.h>
#include <gmock/gmock.h>
using namespace ::testing;
class MockUnit
{
private:
static MockUnit *mockInstance;
public:
static MockUnit& getInstance()
{
if (!mockInstance)
{
mockInstance = new MockUnit();
}
return *mockInstance;
}
static void cleanup()
{
delete mockInstance;
mockInstance = nullptr;
}
MOCK_METHOD(int, ext_sum,(int, int));
MOCK_METHOD(int, ext_sum2, ());
};
MockUnit *MockUnit::mockInstance = nullptr;
extern "C"
{
int ext_sum(int a, int b) {return MockUnit::getInstance().ext_sum(a, b);}
int ext_sum2() {return MockUnit::getInstance().ext_sum2();}
#include "sum.c"
}
class SumTest : public ::testing::Test {
protected:
void SetUp()
{
}
void TearDown()
{
MockUnit::getInstance().cleanup();
}
};
TEST_F (SumTest, PositiveTest)
{
InSequence seq;
EXPECT_CALL(MockUnit::getInstance(), ext_sum(1, 2)).Times(AnyNumber()).WillOnce(Return(3));
EXPECT_CALL(MockUnit::getInstance(), ext_sum2());
EXPECT_EQ(3, sum(1, 2)) << "Positive Test";
}
TEST_F (SumTest, NegativeTest)
{
EXPECT_CALL(MockUnit::getInstance(), ext_sum(_, _)).Times(AnyNumber()).WillOnce(Return(-3));
EXPECT_EQ(-3, sum(-1, -2));
EXPECT_CALL(MockUnit::getInstance(), ext_sum(_, _)).Times(AnyNumber()).WillOnce(Return(-5));
EXPECT_EQ(-5, sum(-2, -3));
EXPECT_CALL(MockUnit::getInstance(), ext_sum(_, _)).Times(AnyNumber()).WillOnce(Return(-300));
EXPECT_EQ(-300, sum(-100, -200));
}
TEST_F(SumTest, TestSum)
{
EXPECT_CALL(MockUnit::getInstance(), ext_sum(_, _)).Times(AnyNumber()).WillOnce(Return(9));
ASSERT_GT (10, sum(1, 2));
}
int main(int argc, char **argv)
{
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
MOCK을 사용하기 위해 MOCK 클래스를 지정하고 각 함수에 대해 다시 정의한다. 위에서 설명하지 못한 테스트 픽스쳐를 활용하여 각 테스트 케이스가 시작할 때, 끝날 때 할 수 있는 행동을 지정한다. 테스트 픽스쳐를 사용하려면 TEST() 대신 TEST_F()를 사용해야 한다. TEST_F(%테스트 픽스쳐%, %테스트 케이스%)를 사용하면 된다.
Google Mock(GMock)은 워낙 다양하게 사용할 수 있기 때문에 다 나열할 수는 없고, 내가 주로 발견한 방법이나 팁 위주로 아래에 업데이트 하겠다.
https://github.com/google/googletest/blob/master/googlemock/docs/cheat_sheet.md
사실 위 링크에 모든 사용법이 나오나, 예시 없이 한 번에 알아듣기는 힘들다.
EXPECT_CALL(mock-object, method (matchers)?) 은 MOCK 함수가 matchers의 조건을 만족하면서 불리는지를 판별한다.
위에서 EXPECT_CALL(MockUnit::getInstance(), ext_sum(1, 2))의 경우, ext_sum이라는 함수의 파라미터가 1, 2를 만족하면서 불린 경우가 있을 경우 테스트 통과, 아닐 경우 테스트 실패이다. 파라미터에 상관없이 불렸는지 안 불렸는지만 알고 싶다면 parameter대신 _을 넣으면 된다. EXPECT_CALL(MockUnit::getInstance(), ext_sum(_, _)) 이런식으로.
Times()는 기대조건을 반복하는 용도로 활용된다. 아래는 3번 불렸는지 확인하는 것이다.
EXPECT_CALL(MockUnit::getInstance(), ext_sum(1, 2)).Times(3);
함수가 안 불렸는지도 중요한 테스트 결과값이 될 수 있다. 아래의 경우 ext_sum(1,2)가 한번이라도 불리면 테스트 실패, 안 불리면 테스트 성공이다. EXPECT_CALL(MockUnit::getInstance(), ext_sum(1, 2)).Times(0);
파라미터를 설정하고 싶을 경우는 어떨까?
result= CONFIG_getItem(SOMETHING, &Counter);
위에서 Counter를 어떤 값으로 설정하고 싶다면 아래 표를 활용하자.
SetArgReferee<N>(value) | Assign value to the variable referenced by the N-th (0-based) argument. |
SetArgPointee<N>(value) | Assign value to the variable pointed by the N-th (0-based) argument. |
EXPECT_CALL(Mock::getInstance(), CONFIG_getItem(SOMETHING, _)).WillOnce(SetArgPointee(oldDDRCounter));
와 같이 쓸 경우 Counter이 oldDDRCounter의 값으로 설정되어 함수내에서 사용될 수 있다.
만약 CONFIG_getItem의 두 번째 파라미터 타입이 void * 이고 oldDDRCounter의 타입이 int라면 어쩔까?
ACTION(Sum) { return arg0 + arg1; } | Defines an action Sum() to return the sum of the mock function's argument #0 and #1. |
ACTION_P(Plus, n) { return arg0 + n; } | Defines an action Plus(n) to return the sum of the mock function's argument #0 and n. |
ACTION_Pk(Foo, p1, ..., pk) { statements; } | Defines a parameterized action Foo(p1, ..., pk) to execute the given statements. |
위 표를 활용하여
ACTION_P(SetArg1ToEnum, value) {*static_cast<int*>(arg1)=value;}
EXPECT_CALL(Mock::getInstance(), CONFIG_getItem(SOMETHING, _)).WillOnce(SetArg1ToEnum(oldDDRCounter));
라고 쓰면 된다.
만약 한 번 불릴 때 파라미터 설정과 반환값 설정을 동시에 하고 싶다면 DoAll을 사용하자.
DoAll(a1, a2, ..., an) | Do all actions a1 to an and return the result of an in each invocation. The first n - 1 sub-actions must return void. |