Данная лабораторная работа посвещена изучению фреймворков для тестирования на примере GTest
$ open https://github.com/google/googletest
- 1. Создать публичный репозиторий с названием lab05 на сервисе GitHub
- 2. Выполнить инструкцию учебного материала
- 3. Ознакомиться со ссылками учебного материала
- 4. Составить отчет и отправить ссылку личным сообщением в Slack
$ export GITHUB_USERNAME=<имя_пользователя>
$ alias gsed=sed # for *-nix system
$ cd ${GITHUB_USERNAME}/workspace
$ pushd .
$ source scripts/activate
$ git clone https://github.com/${GITHUB_USERNAME}/lab04 projects/lab05
$ cd projects/lab05
$ git remote remove origin
$ git remote add origin https://github.com/${GITHUB_USERNAME}/lab05
$ mkdir third-party
$ git submodule add https://github.com/google/googletest third-party/gtest
$ cd third-party/gtest && git checkout release-1.8.1 && cd ../..
$ git add third-party/gtest
$ git commit -m"added gtest framework"
$ gsed -i '/option(BUILD_EXAMPLES "Build examples" OFF)/a\
option(BUILD_TESTS "Build tests" OFF)
' CMakeLists.txt
$ cat >> CMakeLists.txt <<EOF
if(BUILD_TESTS)
enable_testing()
add_subdirectory(third-party/gtest)
file(GLOB \${PROJECT_NAME}_TEST_SOURCES tests/*.cpp)
add_executable(check \${\${PROJECT_NAME}_TEST_SOURCES})
target_link_libraries(check \${PROJECT_NAME} gtest_main)
add_test(NAME check COMMAND check)
endif()
EOF
$ mkdir tests
$ cat > tests/test1.cpp <<EOF
#include <print.hpp>
#include <gtest/gtest.h>
TEST(Print, InFileStream)
{
std::string filepath = "file.txt";
std::string text = "hello";
std::ofstream out{filepath};
print(text, out);
out.close();
std::string result;
std::ifstream in{filepath};
in >> result;
EXPECT_EQ(result, text);
}
EOF
$ cmake -H. -B_build -DBUILD_TESTS=ON
$ cmake --build _build
$ cmake --build _build --target test
$ _build/check
$ cmake --build _build --target test -- ARGS=--verbose
$ gsed -i 's/lab04/lab05/g' README.md
$ gsed -i 's/\(DCMAKE_INSTALL_PREFIX=_install\)/\1 -DBUILD_TESTS=ON/' .travis.yml
$ gsed -i '/cmake --build _build --target install/a\
- cmake --build _build --target test -- ARGS=--verbose
' .travis.yml
$ travis lint
$ git add .travis.yml
$ git add tests
$ git add -p
$ git commit -m"added tests"
$ git push origin master
$ travis login --auto
$ travis enable
$ mkdir artifacts
$ sleep 20s && gnome-screenshot --file artifacts/screenshot.png
# for macOS: $ screencapture -T 20 artifacts/screenshot.png
# open https://github.com/${GITHUB_USERNAME}/lab05
$ popd
$ export LAB_NUMBER=05
$ git clone https://github.com/tp-labs/lab${LAB_NUMBER} tasks/lab${LAB_NUMBER}
$ mkdir reports/lab${LAB_NUMBER}
$ cp tasks/lab${LAB_NUMBER}/README.md reports/lab${LAB_NUMBER}/REPORT.md
$ cd reports/lab${LAB_NUMBER}
$ edit REPORT.md
$ gistup -m "lab${LAB_NUMBER}"
- Создайте
CMakeList.txt
для библиотеки banking. - Создайте модульные тесты на классы
Transaction
иAccount
.- Используйте mock-объекты.
- Покрытие кода должно составлять 100%.
- Настройте сборочную процедуру на TravisCI.
- Настройте Coveralls.io.
- Первым делом создаем стандартный файл CMakeLists.txt. Для удобства дальнейшего подключения тестов расположим его в корне репозитория, а не в /banking, где он лежит изначально.
cmake_minimum_required(VERSION 3.4)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
project(banking)
add_library(banking STATIC banking/Account.cpp banking/Transaction.cpp)
target_include_directories(banking PUBLIC banking/)
Указываем имя проекта, какую версию симейка и какой стандарт языка нужно использовать. Затем, добавляем в нашу "библиотеку" два .cpp файла, указываем папку, где проект будет искать включаемые файлы. Чтобы собрать тесты придется исправить этот файл:
cmake_minimum_required(VERSION 3.4)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
option(BUILD_TEST "Build tests" OFF)
project(banking)
add_library(banking STATIC banking/Account.cpp banking/Transaction.cpp)
target_include_directories(banking PUBLIC banking/)
if(BUILD_TESTS)
enable_testing()
add_subdirectory(submodules/gtest)
add_executable(check tests/tests.cpp)
target_link_libraries(check banking gtest_main gmock_main)
add_test(NAME check COMMAND check)
endif()
Теперь мы добавляем секцию if, в которую система входит при сборке если ранее заданная опция "BUILD_TESTS" поставлена в ON (по умолчанию, как видно в 6-й строке она поставлена в OFF). Это можно сделать при сборке указав -DBUILD_TESTS=ON Касаемо содержимого этой секции: сначала включается тестирование для текущего репозитория. Используется она в связке с add_test, указанной далее. Далее подключается поддиректория с библиотекой gtest, указывается файл который будет собираться и указываются библиотеки, которые будут к этому файлу подключаться. Если с banking и gtest_main все понятно, то gmock_main - библиотека, позволяющая создавать mock-объекты которые понадобятся в третьем задании. В add_test мы указываем имя собираемого файла после NAME и после COMMAND. Обычно после COMMAND нужно указать команды для командной строки, но если указать имя собираемого файла, то в конечном итоге туда подставится его положение.
- Модульные тесты.
В конечном итоге мой файл с тестами выглядит так tests/tests.cpp:
#include "Account.h"
#include "Transaction.h"
#include <gtest/gtest.h>
#include <gmock/gmock.h>
class AccountMock : public Account {
public:
AccountMock(int id, int balance) : Account(id, balance) {}
MOCK_CONST_METHOD0(GetBalance, int());
MOCK_METHOD1(ChangeBalance, void(int diff));
MOCK_METHOD0(Lock, void());
MOCK_METHOD0(Unlock, void());
};
class TransactionMock : public Transaction {
public:
MOCK_METHOD3(Make, bool(Account& from, Account& to, int sum));
};
TEST(Account, Mock) {
AccountMock acc(1, 100);
EXPECT_CALL(acc, GetBalance()).Times(1);
EXPECT_CALL(acc, ChangeBalance(testing::_)).Times(2);
EXPECT_CALL(acc, Lock()).Times(2);
EXPECT_CALL(acc, Unlock()).Times(1);
acc.GetBalance();
acc.ChangeBalance(100); // throw
acc.Lock();
acc.ChangeBalance(100);
acc.Lock(); // throw
acc.Unlock();
}
TEST(Account, SimpleTest) {
Account acc(1, 100);
EXPECT_EQ(acc.id(), 1);
EXPECT_EQ(acc.GetBalance(), 100);
EXPECT_THROW(acc.ChangeBalance(100), std::runtime_error);
EXPECT_NO_THROW(acc.Lock());
acc.ChangeBalance(100);
EXPECT_EQ(acc.GetBalance(), 200);
EXPECT_THROW(acc.Lock(), std::runtime_error);
}
TEST(Transaction, Mock) {
TransactionMock tr;
Account ac1(1, 50);
Account ac2(2, 500);
EXPECT_CALL(tr, Make(testing::_, testing::_, testing::_))
.Times(6);
tr.set_fee(100);
tr.Make(ac1, ac2, 199);
tr.Make(ac2, ac1, 500);
tr.Make(ac2, ac1, 300);
tr.Make(ac1, ac1, 0); // throw
tr.Make(ac1, ac2, -1); // throw
tr.Make(ac1, ac2, 99); // throw
}
TEST(Transaction, SimpleTest) {
Transaction tr;
Account ac1(1, 50);
Account ac2(2, 500);
tr.set_fee(100);
EXPECT_EQ(tr.fee(), 100);
EXPECT_THROW(tr.Make(ac1, ac1, 0), std::logic_error);
EXPECT_THROW(tr.Make(ac1, ac2, -1), std::invalid_argument);
EXPECT_THROW(tr.Make(ac1, ac2, 99), std::logic_error);
EXPECT_FALSE(tr.Make(ac1, ac2, 199));
EXPECT_FALSE(tr.Make(ac2, ac1, 500));
EXPECT_TRUE(tr.Make(ac2, ac1, 300));
}
Сначала нужно объявить мок-объекты. Для этого необходимые функции тестриуемого класса должны быть виртуальными, что, я считаю, является главным минусом gmock.
В объявлении присутствует MOCK_METHODn(name, signature)
Вместо n подставляется количество аргументов функции, вместо name - имя без круглых скобок, вместо signature - сигнатура, т. е возвращаемый тип и типы аргументов в скобках.
Важно заметить что наследование происходит с меткой public.
Далее, с помощью EXPECT_CALL и .Times() мы задаем сколько раз будет вызываться данная функция. Если вызвана меньше - тест не пройдет. С помощью testing::_ можно указать что функций будет принимать любые аргументы тех типов, что были указаны. Если не указать сколько раз будет происходить вызов, то, согласно официальной документации gmock поведение не определено. Здесь же можно указать когда и что функция будет возвращать. И вот здесь главная загвоздка - все такие "подставные функции" возвращают только то, что для них укажут. Как в таком случае вообще проводить тестирование для меня остается загадкой.
В конечном итоге есть две версии тестов - с моками и без. С моками чтобы они были, обычная что-то проверяет. С помощью них, кстати, удалось найти ошибку в файле Transaction.cpp - там в функции make в Debit передавался аккаунт того, куда отправлять. Таким образом деньги у первого просто не снимались, если операция вообще происходила.
- Настройка Travis-Ci
Настройка, опять же, происходит по стандартной схеме. Создаем .travis.yml и делаем указываем какими компиляторами и на какой платформе будем собирать. Я взял .travis.yml из л/р №3. В дальнейшем его придется модифицировать чтобы затем запускался coveralls
language: cpp
os:
- linux
addons:
apt:
sources:
- george-edison55-precise-backports
packages:
- cmake
- cmake-data
script:
- cmake -H. -B_build -DBUILD_TESTS=ON
- cmake --build _build
- cmake --build _build --target test -- ARGS=--verbose
Стоит отметить третью строку с конца. В ней мы переключаем ту самую переменную, которую ранее указали в CMakeLists.txt, позволяя собрать тесты.
- Coveralls.io
Первым делом нужно добавить файл .coveralls.yml в корень репозитория Его содержимое:
service_name: travis-pro
repo_token: wL1JVmhm7eoUktc7yct8iutKZ9e5JBRmN
О его необходимости в интернете есть очень много мнений, но от себя скажу, что а) без него ничего не запускается в coveralls, даже если правильно все указать в CMakeLists.txt и .travis.yml б) без токена так же ничего работать не будет. А указывать токен, раз репозиторий открытый, мне хотелось несильно. Далее исправим CMakeLists.txt:
option(COVERAGE "Check coverage" ON)
...
if (COVERAGE)
target_compile_options(check PRIVATE --coverage)
target_link_libraries(check --coverage)
endif()
Это нужно, чтобы при компиляции создавались определенные файлы, нужные для coveralls. Теперь .travis.yml:
before_install:
- pip install --user cpp-coveralls
...
after_success:
- coveralls --root . -E ".*gtest.*" -E ".*CMakeFiles.*"
Здесь мы загружаем cpp-coveralls и затем, после прохождения проверяем coverage, не включая файлы gtest и CMake с помощью флага -Е (exclude) В итоге, на сайте coveralls.io выводится покрытие кода. Вот мои результаты:
Gmock наверное, не нужен. Во многом потому что, как правило, приходится слишком много всего менять в коде.
Coveralls было очень трудно настроить. В итоге получилось, что я набрал что-то из всех найденных мною методов и оно каким-то чудом заработало. А методов, то есть материалов по coveralls, было очень и очень немного.
Copyright (c) 2015-2019 The ISC Authors