Skip to content

Автоматическое тестирование в QReal

jzuken edited this page Sep 1, 2012 · 15 revisions

Модульное тестирование (юнит-тестирование, unit testing) позволяет проверить правильность работы отдельных модулей программы. Главная его идея заключается в том, что необходимо написать небольшие тесты для каждой нетривиальной функции или метода исходного кода программы. Результат исполнения этих тестов позволит достаточно быстро проверить работоспособность программы, а также понять, не появились ли ошибки в уже оттестированных местах (регрессионное тестирование). Также модульные тесты являются хорошим примером использования тестируемых методов и функций, т.е. их можно рассматривать как средство косвенной документации. Также тесты заставляют разработчиков делать классы и их методы более мелкими, что чаще всего приводит к более гибкой архитектуре.

Для характеристики тестов часто используют аббревиатуру FIRST:

  • Fast — тест должен исполняться быстро. Чем дольше запускается тест, тем реже его будут запускать разработчики, а значит, тем меньше от него толка.
  • Independent — тест не должен зависеть от какого-то другого теста, от порядка выполнения тестов или от текущего окружения. Все окружение должно быть либо аккуратно настроено, либо заменено мок-объектами (особое внимание должно быть уделено тому, чтобы тест не зависел от текущих настроек SettingsManager'а).
  • Repeatable — тест должен выдавать один и тот же ответ при любых условиях запуска и при любом количестве последовательных запусков.
  • Self-checking — тест должен сам автоматически проверять корректность своего запуска и оповещать о дефектах посредством адекватных сообщений.
  • Timely — тест должен писаться одновременно с кодом (либо до, либо после кода). Тестироваться должно все, что может быть протестировано текущими средствами.

В результате проведённого анализа для использования в QReal был выбран Google C++ Mocking Framework.

Как всё собрать и запустить?

Сборка

Для того, чтобы собрать юнит-тесты необходимо присвоить значение TRUE переменной UNIT_TEST в файле qrtest/unitTests/unittestDeclaration.cfg (т.е. написать UNIT_TEST = TRUE). Этот файл добавлен в проект unitTests.pro, поэтому в QtCreator'е его можно найти по такому пути: qrtest/unitTests/Other files.

После этого нужно просто собрать весь проект qreal. Всё необходимое для тестов скомпилируется автоматически в нужном порядке. Исполнямые файлы тестов можно будет найти в папке bin/unittests (qrlibs_unittests, qrxc_unittests, qrgui_unittests). Тесты и тестируемый код лучше собирать в режиме отладки, т.к. некоторые тесты проверяют срабатывание Q_ASSERT'ов, которые под релизом просто превращаются в ничего, и соответствующие тесты не сработают.

Для автоматического тестирования в CI-системе достаточно собрать проект qrealTest.pro, потом пойти в папку bin/unittests, запустить 3 файла с тестами и сделать общий отчёт.

Замечание: включённые тесты позволяют работать с основным кодом и дальше. Просто нужно выбрать правильный проект для запуска. В данном случае, это будет возвращение к qrgui.

Запуск

Их можно запускать как напрямую из консоли (для linux, т.к. иначе нужно делать статическую линковку с Qt-библиотеками), так и из QtCreator'а. Для этого нужно выбрать в меню Run пункт unitTests, qrguiTest или qrxcTest, либо просто нажать правой кнопкой в браузере открытых проектов на соответствующих папках и нажать Run там. Отчёт о прохождении тестов будет выведен в консоль QtCreator'а.

При запуске тестов из консоли отчёт можно вывести в формате xml в отдельный файл. Например, для qrlibs_unittests, нужно запустить qrlibs_unittests с параметром --gtest_output=xml, тогда отчёт будет в файле test_detail.xml в той же папке, что и исполняемый файл с тестами. --gtest_output=xml[:DIRECTORY_PATH/|:FILE_PATH] делает отчёт с заданным именем или в заданной папке.

Это можно сделать и в QtCreator'е. Для этого нужно открыть в главном окне закладку Project (Ctrl+5), перейти на вкладку Run Settings, выбрать в графе Run configuration нужный проект с тестами (перечислялись выше) и добавить в графу Arguments нужную опцию (либо --gtest_output=xml, либо более сложную).

Также для запуска тестов можно использовать GUI gtest-gbar-1.2.2. Для этого, нужно скачать оттуда архив, распаковать, собрать соответствующую программу из исходников на C#, запустить её, указать путь до исполняемого файла с тестами и нажать Go.

Расположение кода с тестами

При тестировании классов, находящихся в модуле qrgui, свой код необходимо размещать в проекте qrguiTest в папке unitTests. Таким образом, эти заголовочные файлы и файлы с исходном кодом необходимо добавлять в qrgui/unitTests/unitTests.pri (qrguiTest/unitTests/unitTests.pri в браузере проектов QtCreator'а). Полностью аналогичная ситуация обстоит и с тестами для qrxc. Соответствующий код нужно добавлять в qrxc/unitTests/unitTests.pri (qrxcTest/unitTests/unitTests.pri).

При тестирования qrkernel, qrutils, qrrepo, qrmc и плагинов, свой код необходимо класть в проект qrtest/unitTests/unitTests.pro, соблюдая при этом корректное размещение по подкаталогам и прочие правила, написанные ниже. Например, тесты для класса Exception в qrkernel/exception/exception.h должны лежать в папке qrtest/unitTests/qrkernelTests/exception, а тесты для класса BlockParser в plugins/blockDiagram/visualDebugSupport/interpreter/blockParser.h должны лежать в папке qrtest/unitTests/visualDebugSupportPluginTests/interpreter.

Замечание: для того, чтобы тестирование какого-либо плагина было возможно, необходимо добавить его исходные файлы в проект qrtest/qrSources, собирающий статическую библиотеку из всего, что есть в проекте qreal (за исключением qrgui и qrxc). Это можно сделать путём добавления строчки include(path/to/your/plugin.pro) в qrtest/qrSources/qrSources.pro перед аналогичными строками для qrkernel, qrutils и т.п. Также важно, чтобы пути до исходных файлов в проектном файле подключаемого плагина начинались с $$PWD. Иначе они не смогут быть скопированы корректным образом средствами qmake в qrSources.pro и не будут добавлены в собираемую библиотеку, а, значит, при сборке проекта unitTests возникнет undefined reference.

Как писать тесты?

Для более подробного ознакомления:
gtest
gtest advanced
gmock

Примеры тестов можно найти в официальном репозитории gmock'а и gtest'а, а также в папке qrtest/exampleTests, плюс уже есть некоторое количество реальных тестов.

Для создания простого теста необходимо использовать функцию TEST(), принимающую параметрами название набора тестов и название теста в этом наборе. Наличие названия набора тестов позволяет в дальнейшем исполнять, например, только тесты из этого набора при помощи аргумента командной строки --gtest_filter. Названия у тестов могут быть одинаковые, если имена наборов тестов разные. Одна функция TEST() является одним тестом.

Для того, чтобы использовать одинаковую конфигурацию системы для некоторых тестов, необходимо использовать тестовый класс. Для этого надо отнаследоваться от класса ::testing::Test и переопределить виртуальные методы SetUp() и TearDown() для инициализации окружения системы для каждого теста и для освобождения ресурсов и корректного завершения. Тесты должны объявляться при помощи функции TEST_F() с первым аргументом, равным названию тестового класса. Одна такая функция является одним тестом. Методы SetUp() и TearDown() будут вызываться при исполнении каждого такого теста, т.е. для каждого теста объект тестового класса будет свой. При этом важно, что это название не должно совпадать ни с каким названием тестовых наборов, о которых речь была ранее.

Для того, чтобы что-нибудь проверить на правильность, необходимо использовать функции вида ASSERT_* и EXPECT_*. При несовпадении результата с правильным в первом случае текущий тест завершается с фатальной ошибкой, а во втором случае он продолжается, что позволяет обнаружить и другие ошибки.

Для создания мока класса А необходимо:
- отнаследоваться от класса А
- для каждого виртуального метода класса А в новом классе написать MOCK_METHODn(methodName, ret_value(arguments)), где n - число аргументов метода, methodName - его имя, ret_value - тип возвращаемого значения (void, int, etc), arguments - все аргументы функции.
Таким образом для метода void doSmth(int a, int b) в новом классе должна быть строка MOCK_METHOD2(doSmth, void(int a, int b)). QtCreator может такой синтаксис считать ошибочным, это нормально.
- после этого в файле с тестами сделать include этого нового класса, сделать объект этого класса, задать его предполагаемое поведение, далее написать код самого теста. При уничтожении мок-объекта gmock проверит правильность его поведения.

Живой пример создания теста

Допустим мы хотим протестировать класс utils::ExpressionsParser, находящийся в qrutils/expressionsParser/expressionsParser.h. Как видно из конструктора, ему нужен указатель на объект класса qReal::ErrorReporterInterface, который нужен для учёта ошибок при разборе выражений. Нас же в данном тесте его логика работы совершенно не интересует, к тому же, чтобы получить экземпляр такого класса нужно постараться. Можно, конечно, подставить NULL в конструктор, но это неправильно, т.к. ошибочные случаи тоже необходимо тестировать. Поэтому нужно создать мок-объект класса qReal::ErrorReporterInterface и использовать его. Начнём с него.

Во-первых, создадим файл qrtest/unitTests/mocks/grgui/toolPluginInterface/usedInterface/errorReporterMock.h, согласно рекомендациям по написанию тестов, приведённой ниже (все моки кладутся в папку mocks с сохранением пути). Во-вторых, сделаем мок по инструкции выше. Получили следующее:

class ErrorReporterMock : public qReal::ErrorReporterInterface {   
public:    
    MOCK_METHOD2(addInformation, void(QString const &message, qReal::Id const &position));   
    MOCK_METHOD2(addWarning, void(QString const &message, qReal::Id const &position));   
    MOCK_METHOD2(addError, void(QString const &message, qReal::Id const &position));   
    MOCK_METHOD2(addCritical, void(QString const &message, qReal::Id const &position));   

    MOCK_METHOD0(clear, void());   
    MOCK_METHOD0(clearErrors, void());   
    MOCK_METHOD0(wereErrors, bool());   
};

Далее создадим фиксчуру для нашего теста. Для этого создаём файл qrtest/unitTests/qrutilsTests/expressionsParser/expressionParserTest.h и пишем туда следующее:

class ExpressionParserTest : public testing::Test {

protected:
    virtual void SetUp();

    virtual void TearDown();

    utils::ExpressionsParser *mParser;
    ErrorReporterMock mErrorReporter;
};

Всё объявлено protected для того, чтобы можно было отнаследоваться от этой фиксчуры и сделать что-то новое. Видим виртуальные методы SetUp() и TearDown(), а также поля самого парсера и мока докладчика об ошибках. Объекты мок-класса нельзя создавать при помощи оператора new, потому что подсчёт количества вызовов определённого метода и прочие проверки происходят при вызове деструктора для этого объекта. Теперь перейдём непосредственно к заданию начальной конфигурации и очистки ресурсов в конце, а также к созданию самих тестов.

Для этого требуется создать файл qrtest/unitTests/qrutilsTests/expressionsParser/expressionParserTest.cpp. В нём сначала определим SetUp() и TearDown() следующим образом:

void ExpressionParserTest::SetUp() {
    mParser = new ExpressionsParser(&mErrorReporter);
}

void ExpressionParserTest::TearDown() {
    delete mParser;
}

Таким образом, мы обеспечиваем новый чистый парсер и докладчик об ошибках для каждого теста. Перейдём к написанию самих тестов. Допустим, мы хотим проверить, считает ли парсер выражения вообще. Тогда нужно написать следующую функцию:

TEST_F(ExpressionParserTest, expressionCalculationTest) {
    QString const stream = "(2 + 2) * 3 + 4";
    int pos = 0;

    EXPECT_EQ(mParser->parseExpression(stream, pos).property("Number").toInt(), 16);
}

Разберём данный тест поподробнее. Функцию TEST_F() мы обязаны использовать при наличии фиксчуры. ExpressionParserTest -- это название группы тестов и, по совместительству, название фиксчуры. expressionCalculationTest -- название самого теста, которое говорит нам о том, что будет тестироваться вычислеине значения выражения. EXPECT_EQ(*, *) проверяет, равны ли первый и второй аргумент. Также видим, что аргументы расставлены согласно рекомендациям, т.е. сначала результат работы тестируемого класса, потом желаемое значение.

Теперь напишем тест с использованием мока докладчика, т.е. подсунем в парсер выражение с синтаксической ошибкой.

TEST_F(ExpressionParserTest, parseErrorTest2) {
    EXPECT_CALL(mErrorReporter, addCritical(_, _)).Times(Exactly(1));

    QString const stream = "2**2";
    int pos = 0;

    mParser->parseExpression(stream, pos);
}

Опять видим функцию TEST_F(), название группы тестов ExpressionParserTest и название теста parseErrorTest2. Далее идёт важный момент: всё предполагаемое поведение мок-объектов необходимо описывать до реального использования этих обектов. Поэтому сразу пишем о том, что ждём EXPECT_CALL() у объекта mErrorReporter вызова метода addCritical с любыми параметрами ( _ - ознает любое значение) при этом ровно 1 раз Times(Exactly(1)). При этом ещё необходимо вверху дописать, чтобы использовать данную функциональность так, как написано:

using ::testing::Exactly;
using ::testing::_;

Рекомендации по оформлению тестов

  • На код тестов распространяются общие правила styleguide
  • Название тестовых групп, фиксчур должны иметь в самом конце суффикс слово "Test". Из названия теста должно быть ясно, что тестируется, например, если тестируется метод, то название должно копировать название метода и иметь суффикс "Test".
  • Следует стараться придерживаться правила: один класс -- один тестовый файл или фиксчура.
  • В связи с тем, что qrtest можно тестировать все библиотеки, то тесты следует группировать по ним, создавая соответствующие подкаталоги.
  • Фиксчуры и прочие тестовые классы должны лежать в пространстве имён qrTest.
  • Не следует бояться тестов с большим количеством кода. Чем полнее описана тестовая ситуация и чем больше проверок внутри неё содержится, тем лучше.
  • Если есть некоторая одинаковая конфигурация на большинство тестов в каком-либо тестовом файле, следует использовать тестовую фиксчуру с setUp() и tearDown().
  • Если для нескольких тестов используются одинаковые константы, то можно их вынести в пространство qrTest.
  • Тесты со сложной логикой работы необходимо снабжать комментариями как к самому методу TEST() или TEST_F(), так внутри самого теста.
  • Мок-классы следует снабжать суффиксом "Mock" и класть в папку qrtest/mocks, сохраняя при этом для удобства каталожную структуру пути до исходного класса. Например, если мы хотим создать мок-класс для класса, находящегося в файле a/b/c/d.h, то создаём файл qrtest/a/b/c/dMock.h и следуем инструкциям по созданию мок-класса.
  • При использовании ASSERT_* и EXPECT_* в случае сравнения двух значений всегда первым аргументом должно быть то, что получилось в результате работы, а вторым -- желаемое идеальное значение.
Clone this wiki locally