Тестирование кода на C++ с помощью Google Test

13 ноября 2017

Как и любой уважающий себя язык программирования, C++ имеет фреймворки для написания модульных тестов, и даже не один, а очень много. В рамках этой заметки мы познакомимся с основами использования фреймворка Google Test. Это довольно легковесный, однако не в ущерб удобству и функциональности фреймворк, используемый в Chromium, LLVM, Protobuf, OpenCV, и других проектах. Кроме того, из IDE с ним умеет интегрироваться как минимум CLion.

Fun fact! Если вас интересует написание системных тестов, их намного удобнее писать на высокоуровневом языке вроде Python. В частности, для Python есть хороший тестовый фреймворк PyTest.

Все популярные дистрибутивы Linux имеют пакеты с Google Test. Например, в Ubuntu пакет называется libgtest-dev, а в Arch Linux — gtest.

Если вы используете CMake, то тесты, использующие Google Test, добавляются в проект очень просто:

cmake_minimum_required(VERSION 3.6)

project(cpp-json-example-tests)

find_package(GTest REQUIRED)
find_package(Threads REQUIRED)

set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED on)

include_directories(
    ../include
    ${GTEST_INCLUDE_DIRS}
)

add_executable(
  TestSerialization ./TestSerialization.cpp
  ../src/User.cpp ../src/Date.cpp)

target_link_libraries(
  TestSerialization ${GTEST_LIBRARIES} Threads::Threads)

enable_testing()
add_test(TestSerialization "./TestSerialization")

В качестве примера напишем тесты на сериализацию и десериализацию объектов Date и User из статьи Работа с JSON на C++ при помощи библиотеки RapidJSON:

#include <Date.h>
#include <User.h>
#include <gtest/gtest.h>

class TestSerialization : public ::testing::Test {
public:
    TestSerialization() { /* init protected members here */ }
    ~TestSerialization() { /* free protected members here */ }
    void SetUp() { /* called before every test */ }
    void TearDown() { /* called after every test */ }

protected:
    /* none yet */
};

TEST_F(TestSerialization, DateJson) {
    Date d1(1988, 8, 5);
    rapidjson::Document json = d1.toJSON();
    Date d2 = Date::fromJSON(json);
    ASSERT_EQ(d1, d2);
}

TEST_F(TestSerialization, UserJson) {
    User u1(123, "Alex", 79161234567, Date(1988, 8, 5));
    rapidjson::Document json = u1.toJSON();
    User u2 = User::fromJSON(json);
    ASSERT_EQ(u1, u2);
}

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

Заметьте, что, хотя здесь эта возможность и не используется, в общем случае Google Test позволяет заводить глобальное состояние, используемое разными тестами. Это может быть целесообразным, как минимум, для ускорения тестов. В остальном же приведенный код крайне прост, поэтому разбирать его более подробно я не вижу смысла.

Пример вывода программы:

[==========] Running 2 tests from 1 test case.
[----------] Global test environment set-up.
[----------] 2 tests from TestSerialization
[ RUN      ] TestSerialization.DateJson
[       OK ] TestSerialization.DateJson (0 ms)
[ RUN      ] TestSerialization.UserJson
[       OK ] TestSerialization.UserJson (0 ms)
[----------] 2 tests from TestSerialization (0 ms total)

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

Спрашивается, а зачем нужен этот фреймворк, если я могу написать обычную программу с assert’ами? Во-первых, такая программа завершит свое выполнение после первого упавшего теста, а Google Test всегда выполняет все тесты. Во-вторых, Google Test предлагает кучу готовых макросов для сравнения полученного результата с ожидаемым, например, ASSERT_FLOAT_EQ, ASSERT_DOUBLE_EQ, ASSERT_THROW, ASSERT_NO_THROW, и другие (см раз и два). Думаю, польза от перечисленных макросов понятна из их названий. В-третьих, когда тест падает, Google Test сам выведет вам ожидаемое и реально полученное значение:

[ RUN      ] TestSerialization.DateJson
../tests/TestSerialization.cpp:26: Failure
      Expected: d1
      Which is: Date(year = 1988, month = 8, day = 5)
To be equal to: d2
      Which is: Date(year = 1988, month = 8, day = 6)
[  FAILED  ] TestSerialization.DateJson (0 ms)

Кроме того, Google Test позволяет запускать заданное подмножество тестов:

./TestSerialization --gtest_list_tests
./TestSerialization --gtest_filter="*.User*"

… а также запускать тесты многократно и в псевдослучайном порядке:

./TestSerialization --gtest_repeat=10 --gtest_shuffle \
                    --gtest_random_seed=42

Если генерировать входные данные каждого теста псевдослучайным образом, получится что-то очень похожее на property-based тесты.

Наконец, Google Test умеет генерировать отчеты в формате XML, чтобы его было легче интегрировать с системами непрерывной интеграции (простите за тавтологию), интерактивными средами разработки, и так далее:

./TestSerialization --gtest_output="xml:out.xml"

Резюмируя вышесказанное, Google Test делает всю рутину, позволяя вам сосредоточиться непосредственно на написании тестов. То есть, с ним вам придется писать намного меньше кода, чем без него.

Полную версию исходников к этому посту, как обычно, вы найдете на GitHub. Также вас может заинтересовать заметка Определение степени покрытия кода на C/C++ тестами, если вдруг вы ее пропустили.

А какими тестовыми фреймворками для C++ в это время суток пользуетесь вы?

Метки: , .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.