GTEST 설치하기

: GTEST는 google에서 개발한 C++ testing framework이다. 이를 이용해 손쉽게 TC를 만들어 개발한 코드 또는 API의 유효성을 검증할 수 있다.

다음은 이를 설치하는 방법이다.

sudo apt-get install libgtest-dev
cd /usr/src/gtest
sudo cmake CMakeLists.txt
sudo make

cd ./lib
sudo cp *.a /usr/lib

간단한 예제를 통해 살펴보기

아래와 같이 calculator API가 있다고 가정하자.

// calculator.h
class calculator
{
private:
    /* data */
public:
    int plus(int var1, int var2);
    int minus(int var1, int var2);
    int divide(int var1, int var2);
    int multiply(int var1, int var2);
};
// calculator.cpp
#include <iostream>
#include "calculator.h"

int calculator::plus(int var1, int var2)
{
    std::cout << var1 + var2 << std::endl;

    return var1 + var2;
}

int calculator::minus(int var1, int var2)
{
    std::cout << var1 - var2 << std::endl;

    return var1 - var2;
}

int calculator::divide(int var1, int var2)
{
    std::cout << var1 / var2 << std::endl;

    return var1 / var2;
}

int calculator::multiply(int var1, int var2)
{
    std::cout << var1 * var2 << std::endl;

    return var1 * var2;
}

위의 calculator.h, calculator.cpp 에서 제공하는 plus, minus, divide, multiply API의 동작(간단한 테스트를 위해 divide by zero 등과 같은 것들은 고려하지 않았습니다)을 검증하기 위해 다음과 같이 TC를 작성할 수 있다.

// main.cpp
#include "calculator.h"
#include "gtest/gtest.h"

namespace {

class CalculatorTest : public ::testing::Test
{
    public:
    calculator gCalculator;

    protected:
    CalculatorTest() {}

    virtual ~CalculatorTest() {}
    virtual void SetUp()
    {
        std::cout << "Setup" << std::endl;
    }

    virtual void TearDown()
    {
        std::cout << "TearDown" << std::endl;
    }
};

TEST_F(CalculatorTest, plus_p)
{
    EXPECT_EQ(2, gCalculator.plus(1, 1));
}

} // namespace

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

이렇게 작성한 파일들을 다음의 명령으로 컴파일 및 실행하면

g++ -o main main.cpp calculator.cpp -lgtest -lpthread
./main
./main 
[==========] Running 1 test from 1 test suite.
[----------] Global test environment set-up.
[----------] 1 test from CalculatorTest
[ RUN      ] CalculatorTest.plus_p
Setup
2
TearDown
[       OK ] CalculatorTest.plus_p (0 ms)
[----------] 1 test from CalculatorTest (0 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test suite ran. (0 ms total)
[  PASSED  ] 1 test.

이렇게 TC의 동작 결과를 확인할 수 있다.

More about GTEST

위의 예제에서 EXPECT_EQ()을 통해 TC의 동작을 확인하였다. EXPECT_EQ처럼 사용할 수 있는 구문은 다음과 같다.

ASSERT_@@EXPECT_@@의 차이는 ASSERT_@@의 경우 TC에서 Fail이 발생하였을 경우 해당 위치 아래의 TC들은 더이상 실행하지 않고 종료하는 반면, EXPECT_@@의 경우 TC에서 Fail이 발생하여도 아래의 TC들을 계속해서 실행한다.

이러한 내용들을 아래에서는 ASSERT_@@Fatal assertion, EXPECT_@@Nonfatal assertion으로 부른다.

Basic Assertion

: 참과 거짓을 테스트하는 구문이다.

Fatal assertion Nonfatal assertion Verifies
ASSERT_TRUE(condition); EXPECT_TRUE(condition); condition is true
ASSERT_FALSE(condition); EXPECT_FALSE(condition); condition is false
     

Binary Comparison

: 비교문을 통해 테스트하는 방법입니다.

Fatal assertion Nonfatal assertion Verifies
ASSERT_EQ(val1, val2); EXPECT_EQ(val1, val2); val1 == val2
ASSERT_NE(val1, val2); EXPECT_NE(val1, val2); val1 != val2
ASSERT_LT(val1, val2); EXPECT_LT(val1, val2); val1 < val2
ASSERT_LE(val1, val2); EXPECT_LE(val1, val2); val1 <= val2
ASSERT_GT(val1, val2); EXPECT_GT(val1, val2); val1 > val2
ASSERT_GE(val1, val2); EXPECT_GE(val1, val2); val1 >= val2
     

String Comparison

: String의 내용에 대해서 비교하여 테스트하는 방법입니다.

Fatal assertion Nonfatal assertion Verifies
ASSERT_STREQ(str1,str2); EXPECT_STREQ(str1,str2); 두 개의 C String이 동일한 값을 가지고 있을 때
ASSERT_STRNE(str1,str2); EXPECT_STRNE(str1,str2); 두 개의 C String이 서로 다른 값을 가지고 있을 때
ASSERT_STRCASEEQ(str1,str2); EXPECT_STRCASEEQ(str1,str2); 두 개의 C String이 대/소문자를 고려하지 않고 동일한 값을 가지고 있을 때
ASSERT_STRCASENE(str1,str2); EXPECT_STRCASENE(str1,str2); 두 개의 C String이 대/소문자를 고려하지 않고 다른 값을 가지고 있을 때
     

TEST_F와 TEST의 차이

: 위에서 사용하지는 않았지만 TEST_F외에도 TEST 구문이 존재한다.

TEST_F 의 경우에는 SetUp(), TearDown()을 통해서 TC수행 이전에 필요한 변수, 값들을 설정하고 TC가 끝난 이후에 할당한 변수, 값들을 해제하는 작업을 손쉽게 행할 수 있다. 만약 Instance 생성, Network 연결 등의 작업을 TC 수행 전에 반복적으로 해야하는 경우에는 SetUp()내에 이러한 작업을 해 두면 손쉽게 TC를 진행할 수 있다.

간단하게 말해서

  1. SetUp() 수행
  2. TEST_F(Fixture, add) 수행
  3. TearDown() 수행
  4. SetUp() 수행
  5. TEST_F(Fixture, diff) 수행
  6. TearDown() 수행

위의 순서대로 수행된다고 보면 된다.

TEST의 경우에는 위의 SetUP(), TearDown()이 필요없는 간단한 TC의 경우에 사용하면 좋을 듯 하다.

설치 오류

: 설치 후 gtest를 사용하기위해 #include "gtest/gtest.h" 를 하였을 때, 아래와 같은 오류를 본 적이 있는가?

Googletest/gtets: fatal error: gtest.h: No such file or directory

그렇다면 이 페이지(jayy-h.tistory.com/21)에서 알려주고 있는 설치 가이드를 따르기 바란다. 나 또한 위와 같은 오류를 겪었고, 오랜 시간 발품을 팔아 확인한 솔루션이다.

 

Reference 
googletest 공식문서

valgrind를 이용한 메모리 디버깅

: valgrind는 할당되지 않은 포인터 변수에 값을 넣는 것, malloc과 free를 수행하며 dangling pointer가 발생되거나 이중 free가 발생되는 문제와 같이 memory leak, invalid memory reference 등의 메모리 관련 문제점을 찾아주는 도구이다.

: 다음은 이러한 valgrind를 어떻게 사용하는지를 나타내는 예제이다.

#include <stdlib.h>
#include <iostream>

using namespace std;

int main()
{
    int *pInt = NULL;
    int cnt = 100;
    pInt = (int *)malloc(sizeof(int) * cnt);

    for (int i = 0; i < cnt; i++) {
        pInt[i] = i;
    }

    cout << pInt[cnt] << endl;

    for (int i = 0; i < cnt; i++) {
        free(&pInt[i]);
    }

    return 0;
}
Pre-requisite

valgrind를 사용하기 위해서는 해당 툴이 설치되어있어야 한다.
설치는 다음의 명령어를 통해 할 수 있다.

$ sudo apt  install valgrind

이제 다음의 명령어를 통해 컴파일한다.

jay@jay-VirtualBox:~/tutorial/valgrind$ g++ -o memory -g memory.cpp
jay@jay-VirtualBox:~/tutorial/valgrind$ ls
memory  memory.cpp

그 후, memory leak과 관련된 오류가 발생하였는지 다음의 명령어를 통해 알 수 있다. 이는 valgrind가 해당 프로그램을 수행시켜보며 메모리 관련된 문제가 발생하였는지를 판단하는 것이므로 test-case를 통해 memory leak을 판단하고자 할 때, test-case가 작성한 코드의 모든 범위와 조건들을 충족하는지에 따라 검출되는 버그가 다르다는 것에 유의하길 바란다.

jay@jay-VirtualBox:~/tutorial/valgrind$ valgrind --tool=memcheck --leak-check=yes --leak-resolution=high -v ./memory 
==39434== Memcheck, a memory error detector
==39434== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==39434== Using Valgrind-3.15.0-608cb11914-20190413 and LibVEX; rerun with -h for copyright info
==39434== Command: ./memory
==39434== 
--39434-- Valgrind options:
--39434--    --tool=memcheck
--39434--    --leak-check=yes
--39434--    --leak-resolution=high
--39434--    -v

...

==39604== 99 errors in context 2 of 2:
==39604== Invalid free() / delete / delete[] / realloc()
==39604==    at 0x483CA3F: free (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==39604==    by 0x1092AA: main (memory.cpp:19)
==39604==  Address 0x4dafc84 is 4 bytes inside a block of size 400 free'd
==39604==    at 0x483CA3F: free (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==39604==    by 0x1092AA: main (memory.cpp:19)
==39604==  Block was alloc'd at
==39604==    at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==39604==    by 0x109214: main (memory.cpp:10)

lcov, genhtml을 이용한 coverage 깔끔하게 보기

NOTE
이전의 gocv를 이용한 커버리지 정보 확인과 밀접한 관계가 있는 글 입니다.
이 글을 풍부하게 이해하기 위해서는 gcov 관련 게시글을 보고 오시길 바랍니다.
Pre-requsite

gcov의 결과를 웹 페이지를 통해 깔끔하게 보여주기 위해 lcov를 사용한다.
이를 위해 다음의 명령어를 이용해 lcov 설치를 진행하자.

sudo apt install lcov

우선 다음과 같은 프로그램이 있다고 가정하자.

// main.cpp

#include <iostream>
using namespace std;

void func(int dividend, int divisor)
{
    cout << "Incoming variable: " << dividend << " " << divisor << endl;

    int quotient = 0;
    int remainder = 0;
    try {
        if (divisor == 0)
            throw exception();
        quotient = dividend / divisor;
        remainder = dividend % divisor;
    } catch (...) {
        cerr << "Exception occured!" << endl;
        quotient = 0;
        remainder = dividend;
    }

    switch(remainder) {
    case 0:
        cout << "Dividend is multiplier of divisior" << endl;
        break;
    case 100:
        cout << "Cannot reach here under this circumstances!" << endl;
        break;
    default:
        cout << "Nothing" << endl; 
        break;
    }
}

int main()
{
    for (int i = 0; i < 100; ++i) {
        for (int j = 0; j < 100; ++j) {
            func(i, j);
        }
    }

    return 0;
}

다음의 명령어를 이용해 컴파일을 수행한다.

g++ -o main main.cpp -fprofile-arcs -ftest-coverage -g

여기서 -fprofile-arcs -ftest-coverage 옵션은 소스의 각 베이직 블록에 프로파일링 코드를 삽입하라는 옵션이다.

이 명령을 수행하면 다음과 같은 파일들이 생성된다.

jay@jay-VirtualBox:~/tutorial/gcov$ g++ -o main main.cpp -fprofile-arcs -ftest-coverage -g
jay@jay-VirtualBox:~/tutorial/gcov$ ls
main  main.cpp  main.gcno

./main을 통해 프로그램을 한 번 수행시킨다. 이는 다음의 main.gcda 파일을 생성시킨다.

jay@jay-VirtualBox:~/tutorial/gcov$ ./main
jay@jay-VirtualBox:~/tutorial/gcov$ ls
main  main.cpp  main.gcda  main.gcno

gcov main.cpp를 수행한다.

jay@jay-VirtualBox:~/tutorial/gcov$ gcov main.cpp 
File 'main.cpp'
Lines executed:89.29% of 28
Creating 'main.cpp.gcov'

File '/usr/include/c++/9/iostream'
No executable lines
Removing 'iostream.gcov'

File '/usr/include/c++/9/bits/exception.h'
Lines executed:100.00% of 1
Creating 'exception.h.gcov'

jay@jay-VirtualBox:~/tutorial/gcov$ ls
exception.h.gcov  main  main.cpp  main.cpp.gcov  main.gcda  main.gcno

lcov --rc lcov_branch_coverage=1 --capture --directory ${gcov_data_file_directory} --output-file {output_file_name} 명령을 통해 gcov 데이터 파일을 이용해 genhtml에 쓰일 파일을 생성한다.

jay@jay-VirtualBox:~/tutorial/gcov$ lcov --rc lcov_branch_coverage=1 --capture --directory ./ --output-file generated.info
Capturing coverage data from ./
Found gcov version: 9.3.0
Using intermediate gcov format
Scanning ./ for .gcda files ...
Found 1 data files in ./
Processing main.gcda
Finished .info-file creation
jay@jay-VirtualBox:~/tutorial/gcov$ ls
exception.h.gcov  main      main.cpp.gcov  main.gcno
generated.info    main.cpp  main.gcda

여기서 --rc lcov_branch_coverage=1 옵션은 분기 커버리지를 보기 위한 옵션으로 genhtml에서도 쓰인다.

마지막으로 genhtml ${info_file} --branch-coverage --output-directory ${output_directory} 명령으로 coverage 정보를 담는 html 파일을 만든다.

jay@jay-VirtualBox:~/tutorial/gcov$ genhtml generated.info --branch-coverage --output-directory ./
Reading data file generated.info
Found 2 entries.
Found common filename prefix "/usr/include/c++/9"
Writing .css and .png files.
Generating output.
Processing file /home/jay/tutorial/gcov/main.cpp
Processing file bits/exception.h
Writing directory view page.
Overall coverage rate:
  lines......: 89.7% (26 of 29 lines)
  functions..: 100.0% (3 of 3 functions)
  branches...: 76.9% (10 of 13 branches)
jay@jay-VirtualBox:~/tutorial/gcov$ ls
amber.png         generated.info     index-sort-f.html  main.gcda
bits              glass.png          index-sort-l.html  main.gcno
emerald.png       home               main               ruby.png
exception.h.gcov  index.html         main.cpp           snow.png
gcov.css          index-sort-b.html  main.cpp.gcov      updown.png

이렇게 되면 다음과 같이 coverage 정보를 시각적으로 아름답게 표현해주는 html 파일이 생성되었다.

 

genhtml을 통해 생성된 파일

gcov를 통한 커버리지 정보 알아보기

: gcov는 gcc에 포함된 유틸리티로 코드 커버리지를 파악하는 데 쓰인다. 이는 어떤 부분이 얼마만큼 수행되는지, 어느 부분이 한 번도 수행되지 않는지를 파악할 수 있고, 이런 정보를 이용해 불필요한 코드 제거에 사용되어질 수 도 있다. 앞서 설명한 gprof와 비교하자면, gprof는 함수 단위의 프로파일링 정보만을 제공하는 반면에 gcov는 베이직 블록 단위의 프로파일링을 수행해 함수 내 어떤 루프가 많이 수행되었는지, if-else 블록에서 어느 블록이 많이 수행되었는지를 알 수 있다.

// main.c
#include <stdio.h>

void func1()
{
    printf("Hello World \n");
}

void func2(int delay)
{
    printf("Delay: %d", delay);

    while (delay--);
}

int main()
{
    for (int i = 0; i < 10; ++i) {
        func1();
    }

    for (int i = 0; i < 100; ++i) {
        func2(i);
    }

    return 0;
}

위와 같은 파일이 있다고 할 때, gcov를 이용한 라인 커버리지를 확인하는 방법은 다음과 같다.

  1. gcc -o main main.c -fprofile-arcs -ftest-coverage -g 명령으로 컴파일을 수행한다.
    • -fprofile-arcs -ftest-coverage 옵션은 소스의 각 베이직 블록에 프로파일링 코드를 삽입하라는 옵션이다.
  2. ./main을 통해 프로그램을 한 번 수행시킨다.
    • 이는 main.gcda, main.gcno 파일을 생성시킨다. 이 두 파일에 소스파일의 베이직 블록에 대한 프로파일링 정보가 포함된다.
  3. gcov main.c 명령을 수행하여 main.c.gcov파일을 생성시킨다.
  4. cat main.c.gocv 명령을 통해 프로파일링 정보를 확인한다.
  1: 1: #include <stdio.h>
  -: 2: 
  -: 3: void func1()
 10: 4: {
 10: 5:     printf("Hello World \n");
 10: 6: }
  -: 7: 
  -: 8: void func2(int delay)
100: 8: {
...
  • 각 라인의 맨 앞에 있는 숫자는 해당 라인이 몇 번 호출되었는지를 나타낸다.

+ Recent posts