[TDD/C++] C++로 실습하는 Test-Driven Development - GoogleTest를 이용한 TDD

2022. 8. 29. 20:0003. Resources/C, C++

728x90
반응형

어떤 라이브러리로 TDD 개념을 적용할 수 있을까?

강의에서는 GoogleTest를 이용해서 TDD개념을 C++ 프로젝트에 적용할 수 있다고 했다.

 

GoogleTest란?

google에서 만든 c++ test framework이다.

소스코드 레포지토리는 다음과 같고,

https://github.com/google/googletest

 

GitHub - google/googletest: GoogleTest - Google Testing and Mocking Framework

GoogleTest - Google Testing and Mocking Framework. Contribute to google/googletest development by creating an account on GitHub.

github.com

유저 가이드는 다음과 같다.

https://google.github.io/googletest/

 

GoogleTest User’s Guide

GoogleTest - Google Testing and Mocking Framework

google.github.io

 

GoogleTest의 특징

소스코드 레포지토리에 설명되어있는 GoogleTest의 특징은 다음과 같다

- xUnit test framework

- Test discovery (뭔지 모르겠다 ㅎ)

- A rich set of assertions

- User-defined assertions

- Death tests (이건 또 뭘까)

- Fatal and non-fatal failures

- Value-parameterized tests

- Type-parameterized tests

- Various options for running the tests

- XML test report generation

 

굉장히 다양한 테스트들을 지원하고, 리포트도 생성해준다고 한다.

 

GoogleTest Build 하는 법

해당 레포를 이용하려면 소스 빌드를 해야한다. CMake로 구성된 프로젝트이기 때문에, 다른 여타 프로젝트들과 마찬가지로 빌드하면 된다.

 

먼저, 레포지토리를 원하는 위치에 클론한다.

git clone https://github.com/google/googletest.git

클론한 레포지토리의 디렉토리로 들어가서 build 준비를 한다.

cd googletest && mkdir build && cd build

CMakeLists.txt를 실행시킨다.

cmake ..

build 폴더에 생성된 MakeFile을 실행시킨다.

make

이렇게 하면 googletest 라이브러리 파일들이 생성된다.

 

GoogleTest의 Asserts

매크로

테스트의 통과 여부를 결정하는 두 가지의 매크로를 제공한다.

- ASSERT_* : 현재 테스트가 fail이면 프로그램을 abort 시킨다.

- EXPECT_* : 현재 테스트가 fail이더라도 프로그램을 abort 시키지 않는다.

그래서 강의에서는 EXPECT 매크로를 쓰는 것을 권장했다.

 

비교 타입

총 네 가지의 비교 타입을 제공한다.

- Basic : true, false를 리턴하는지 확인한다.

TEST ( Examples, BasicAssertExamples ) {
    ASSERT_TRUE(1==1); // passes!
    ASSERT_FALSE(1==2); // passes!
    ASSERT_TRUE(1==2); // fails!
    ASSERT_FALSE(1==1); // fails!
}

- Binary : 부등식 표현으로 결과를 비교한다. (쉘스크립트에서 숫자 비교하는 조건문과 비슷하다)

TEST ( Examples, BinaryAssertExamples) {
    ASSERT_EQ(1,1);
    ASSERT_NE(1,2);
    ASSERT_LT(1,2);
    ASSERT_LE(1,1);
    ASSERT_GT(2,1);
    ASSERT_GE(2,2);
}

- String : C style string을 이용하여 비교한다. (아마  std::string("1") 뒤에는 .c_str()이 붙어야할 것이다)

TEST( Examples, StringAssertExamples) {
    ASSERT_EQ(std::string("1"), std::string("1"));
    ASSERT_NE(std::string("a"), std::string("b"));
    ASSERT_STREQ("a", "a");
    ASSERT_STRNE("a", "b");
    ASSERT_STRCASEEQ("A", "a");
    ASSERT_STRCASENEQ("A", "b");
}

- Floats/Doubles : 두 float 이나 double을 비교하는데, 거어어어의 같은지 확인한다. 거어어어의를 판단하는 기준치도 제공할 수 있다.

 TEST( Examples, FloatDoubleAssertExamples) {
     ASSERT_FLOAT_EQ( 1.0001f, 1.0001f );
     ASSERT_DOUBLE_EQ ( 1.0001, 1.0001 );
     ASSERT_NEAR(1.0001, 1.0001, .0001); // passes
     ASSERT_NEAR(1.0001, 1.0003, .0003); // fails
 }

 

Exception에서도 assert를 확인할 수 있다.

TEST( Examples, FloatDoubleAssertExamples) {
    ASSERT_THROW( callIt(), ReallyBadException);
    ASSERT_ANY_THROW( callIt() );
    ASSERT_NO_THROW( callIt() ); // passes
}

 

Test Doubles 이란

만드는 시스템은 일반적으로 다른 시스템에 의존하는 부분이 있기 마련이다.

그런데 다른 파트의 시스템을 복제하기가 어려우니 간단하게 구현해서 테스트에 이용하는 것을 Test Doubles 라고 말한다.

 

Test Doubles 의 종류

총 5가지의 Test Double에 대해서 설명해준다.

- Dummy: 테스트에 필요해서 만들어두지만, 테스트에 사용되지 않는 객체 (Objects that can be passed around as necessary but to not have any type of test implementatoin and should never be used)

class MyDummy : public MyInterface {
public:
	void SomeFunction() { Throw "I shouldn't be called!";}
};

// Dummy objects expect to never be used and will generally throw an exception 
// if one of their methods is actually called.

 

- Stub: 테스트에 필요한 정해진 답을 리턴하는 기능만 구현된 객체 (These objects provide implementations with canned answers that are suitable for the test)

class MyStub : public MyInterface {
public:
	int SomeFunction(){ return 0;}
};

// Stubs are different than dummy test doubles 
// in that they do expect to be called and return canned data

 

- Fake: 양산이 아닌 테스트에만 적합하게 간단한 특정 기능만 구현해둔 객체 (These object generally have a simplified functional implementation of a particular interface that is adequate for testing but not for production)

class MyTestDB : public DBInterface{
public:
    void pushData(int data){ dataItems.push_back(data);}
protected:
    vector<int> dataItems;
};

// Fake objects provide what is usually a simplified implementation of and interface
// that is functional but not appropriate for production
// (i.e. an in memory database).

 

- Spies: 테스트에 필요한 값을 받아서 저장하고, 나중에 가져다 쓸 수 있는 정도의 구현이 된 객체 (These objects provide implementations that record the values that were passsed in so they can be used by the test)

class MySpy : public MyInterface{
public:
    int savedParam;
    void SomeFunction(int param1) { savedParam = param1;}
};

 

- Mocks: 특정 호출들과 파라미터들이 미리 구현되어있고, 필요하다면 예외까지 리턴할 수 있는 객체 (These objects are pre-programmed to expect specific calls and parameters and can throw exceptions when necessary)

class MyMock : public MyInterface{
public:
    void SomeFunction( int param1 ) {
        if (1 != param1)
            throw "I shouldn't be called!";
    };
};

// Mock objects are the most intelligent test double.
// They are setup with expectations on how they will be called
// and will throw exceptions when those expectations are not met.

 

위와 같이 Test에 필요한 외부 인터페이스를 테스트에 맞게 구현한 것을 Test Double이라고 부르고, 이 강의에서는 GoogleMock을 이용하여 Mocks를 구현해서 테스트에 이용하는 것을 보여줬다.

 

GoogleMock 이란

구글에서 만든 C++ Mocking Framework이다. 테스트 프레임워크가 있다면 반드시 있어야할 것 같은데 역시나 있다. 갓글..

물론 GoogleTest에서만 쓸 수 있지는 않다. 다른 어떠한 C++ unit test framework랑도 호환된다고 한다.

 

GoogleMock의  workflow는 어떻게 될까?

먼저, Mock이 대체해야하는 인터페이스를 상속받아서 Mock 클래스를 선언한다.

그리고, Mock Class에서는 MOCK_METHODn 매크로로 어떤 method가 mocked 될 것인지 정해준다.

Test에 Mock class의 instance를 만들어서 사용하면 된다.

 

GoogleMock Expectations 란?

위에서 만든 Mock class의 instance에서 method를 호출하기 위해서 사용한다.

단어를 정말 잘 골랐다고 생각하는게, 결국 외부에서 들어오길 바라는 "기댓값"을 구현한 것이니까 Expectation이라는 단어가 찰떡이다.

무튼 테스트 내부에서 EXPECT_CALL이라는 매크로를 이용해서 사용하면 된다.

EXPECT_CALL(myMockObj, getData());

사용할 인스턴스와 그 인스턴스에서 사용할 method를 넣어주면 된다.

 

Matchers란?

파라미터를 넣어줘야하는 함수를 사용해야할 때에는 Matchers를 이용하면 된다.

Matchers가 파라미터를 굉장히 다양하게 사용할 수 있도록 도와준다.

EXPECT_CALL(myMockObj, setData(_));

위의 예제에서 "_" 는 아무 값을 의미한다. 이 역시 googletest에서 지원하는 기능이다!

EXPECT_CALL(myMockObj, setData(100));
EXPECT_CALL(myMockObj, setData( Ge(100) ));
EXPECT_CALL(myMockObj, setData( NotNull(myObj) ));

 

매크로를 다채롭게 해줄 Actions 

GoogleMock에 여러가지 행동 옵션을 추가할 수 있는게 Action들이다.

크게 세가지 Action을 소개해주는데, 

- WillOnce: 딱 한 번 실행시킬 것이다.

- WillRepeatedly: 반복적으로 실행시킬 것이다.

- Times: 몇 번 실행시킬 것이다.

EXCEPT_CALL(myMockObj, getData()).WillOnce(Return(1));
// Return 1

EXCEPT_CALL(myMockObj, getData()).WillRepeatedly(Return(1));
// Return 1

EXCEPT_CALL(myMockObj, getData()).WillOnce(Return(1))
                                 .WillOnce(Return(2))
                                 .WillOnce(Return(3));
// Return 1 2 3

EXCEPT_CALL(myMockObj, getData()).Times(4)
                                 .WillRepeatedly(Return(1));
// Return 1 1 1 1

 

정리

여기까지 실습을 제외한 텍스트로 설명해준 내용들을 정리해봤다.

이런 정보들을 이용해서 C++에 TDD를 적용할 수 있다.

 

마지막으로 어떠한 점들을 명심해야하는지 정리하면서 포스팅을 마무리해본다!

할 수 있는 가장 단순한 테스트를 다음 테스트로 하라

이렇게 해야 코드의 복잡도를 점진적으로 늘려나갈 수 있다.

만약 복잡한 테스트케이스로 바로 넘어가게 된다면, 한 번에 구현해야하는 기능이 너무 많아져서 멈춰버릴 수 있다.

게다가 개발 속도가 지연될 뿐 아니라, 디자인 자체가 안좋아질 수도 있다.

 

딱 보면 이해하기 쉬운 테스트 이름을 써라

코드는 쓰는 것은 한 번이지만, 읽히는건 수천번이다. 심지어 협업하는 경우에는 더더욱!

그래서 최대한 명확하고 읽을 수 있게 코드를 작성하자!

TDD에서는 unit test들이 코드의 동작을 이해하는 가장 좋은 문서가 될 것이다.

Test의 이름들은 테스트 될 기능을 최대한 잘 묘사해야한다!

 

빠르게 테스트를 해라

TDD의 가장 큰 장점은 내 변화가 기능들에 미치는 영향을 바로바로 볼 수 있다는 점이다.

따라서, 빌드부터 테스트까지 수 초 내로 진행되어야한다.

이를 위해서는 콘솔로 출력되는 내용들을 최소화할 필요가 있고, mock들을 최적화할 필요도 있다.

 

코드 커버리지 툴을 쓰자

테스트 코드를 작성하면서 모든 테스트를 했다고 생각할 수 있다.

빈틈을 메우기 위해서 코드 커버리지 툴을 쓰자. 특히 동작하는 경우 말고 동작하지 않는 경우에 대한 테스트가 누락되는 경우가 있기 때문이다.

100%의 코드 커버리지를 위해 노력해보자.

 

테스트를 무수히 많이 반복하고, 순서도 뒤섞어 가면서 테스트 해라

테스트를 무수히 반복하는 것은 간헐적으로 테스트를 까먹는 경우를 없애는데 아주 중요하다.

그리고 순서를 섞어가면서 테스트하는 것은 테스트의 디펜던시를 확인하는데 도움이 된다. 테스트끼리는 독립적이어야한다!

이 때,  gtest_repeat나 gtest_shuffle등의 파라미터를 test executable을 실행시킬 때 추가하면 된다.

반응형