Модульное тестирование кода на C++ с помощью Qt Test. Часть 2. Расширенное тестирование

Добавлено 3 января 2020 в 19:06

В данном руководстве рассматриваются более сложные темы модульного тестирования кода на C++ с помощью Qt Test. Обсуждается и подробно анализируется рабочий пример. Предоставляется полный проект qmake и исходный код на C++.

Больше модульного тестирования кода на C++ с помощью Qt Test

В данном руководстве я собираюсь представить более продвинутые функции Qt Test, фреймворка Qt для модульного тестирования кода на C++. В частности, мы рассмотрим, как работать с проектом с несколькими юнит-тестами, и как реализовать тестирование, управляемое данными. Также мы рассмотрим примеры других макросов для тестирования и интеграцию, предлагаемую Qt Creator.

Это второй пост из серии, посвященной Qt Test. Посты этой серии:

Чтобы понять новые концепции, представленные в данной статье, рекомендую прочитать предыдущую статью этой серии про модульное тестирование кода на C++ с помощью Qt Test.

Создание улучшенного проекта

Как было показано в первом руководстве, философия Qt Test заключается в том, что каждый тестовый случай является независимым исполняемым файлом. В реальных проектах у вас обычно есть сотни или даже тысячи различных юнит-тестов, и запускать их все вручную определенно не лучший вариант. Лучший способ запускать их и управлять ими – это создать «родительский» проект. Правильный выбор в этом случае – шаблон «Проект с поддиректориями» (Subdirs Project), который находится в группе «Другой проект» (Other Project) диалогового окна «Новый проект» (New Project).

Рисунок 1 Создание проекта с поддиректориями в Qt Creator
Рисунок 1 – Создание проекта с поддиректориями в Qt Creator

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

Рисунок 2 Проекты в Qt Creator
Рисунок 2 – Проекты в Qt Creator

Для этого руководства я расширил проект юнит-тестов TestCalculator и создал новый под названием TestIntBitset. В новом проекте тестируется упрощенная реализация битового набора. Повторюсь еще раз, код для тестирования (класс IntBitset) включен в проект модульного тестирования для упрощения.

Тестирование на основе данных

Продвинутая функция Qt Test – это тестирование на основе данных. Идея состоит в том, чтобы разделить тесты и данные, чтобы избежать длинного списка похожих макросов QVERIFY или QCOMPARE и повторения всего кода, необходимого для инициализации теста.

Чтобы предоставить данные тестовой функции, вы должны создать еще один частный слот с таким же именем функции, но с дополнительным суффиксом "_data". Например, функцией данных для testDiff() будет testDiff_data().

Реализация функции данных немного похожа на вставку данных в базу данных. Сначала вы определяете данные, как если бы вы определяли таблицу:

void TestCalculator::testDiff_data()
{
    QTest::addColumn<int>("a");
    QTest::addColumn<int>("b");
    QTest::addColumn<int>("result");

Затем добавляете строки значений:

    QTest::newRow("all 0") << 0 << 0 << 0;
    QTest::newRow("same number") << 10 << 10 << 0;
    // ... more data ...
}

Каждая строка содержит имя и список значений. Вы можете представить, что приведенный выше код преобразован во что-то вроде следующей таблицы:

ИндексНазваниеabresult
0“all 0”000
1“same number”10100

После того как мы определили функцию данных, мы можем написать тестовую функцию, которая разделена на 2 части.

Первая часть извлекает данные:

void TestCalculator::testDiff()
{
    // извлечение данных
    QFETCH(int, a);
    QFETCH(int, b);
    QFETCH(int, result);

    // установка значений
    mCalc.SetA(a);
    mCalc.SetB(b);

Вторая часть использует эти данные для выполнения проверок:

    // тест
    QCOMPARE(mCalc.Diff(), result);
}

Без подхода, основанного на данных, нам пришлось бы многократно повторять инструкции по установке значений и проверке QCOMPARE.

Когда выполняется тест, управляемый данными, функция тестирования вызывается один раз для каждого набора данных, и сообщения журнала выглядят следующим образом:

PASS : TestCalculator::testDiff(all 0)
PASS : TestCalculator::testDiff(same number)
... еще строки ...

Как вы можете заметить, чтобы помочь вам различать случаи, в журнал заносится имя строки данных.

Другие полезные макросы

Qt Test предлагает несколько дополнительных макросов, которые помогут вам справиться с различными ситуациями в ваших юнит-тестах.

Провал теста

Один из этих макросов – QFAIL делает текущий тест проваленным. Его можно использовать, когда вы знаете, что что-то приведет к провалу теста. В этом случае нет смысла тратить время на выполнение теста, вы можете просто провалить его и двигаться дальше.

void TestIntBitset::initTestCase()
{
    if(sizeof(int) != 4)
        QFAIL("Int size is not 4 on this platform.");
}

В примере проекта я использовал QFAIL в функциях initTestCase и cleanupTestCase, которые представляют собой специальные функции, выполняемые до и после выполнения тестовых функций. Когда initTestCase завершается провалом, в тестовом случае не выполняется ни один из тестов.

Провал одной проверки

Если вы знаете, что конкретный QVERIFY или QCOMPARE завершится провалом, но вы всё равно хотите продолжить выполнение теста, вы можете выполнить проверку с помощью макроса QEXPECT_FAIL:

void TestIntBitset::testSetOff()
{
	mBS.setAllOn();

	unsigned int bitsOff = 0;
	mBS.setBitOff(BITS_IN_BYTE * bitsOff++);
	mBS.setBitOff(BITS_IN_BYTE * bitsOff++);

	QEXPECT_FAIL("", "isAnyOff not implemented yet", Continue);
	QVERIFY(mBS.isAnyOff());

	// ... еще тестовый код ...
}

Первый параметр этого макроса определяет строку данных при выполнении тестирования на основе данных, но при обычном тестировании он может быть установлен на пустую строку. Второй параметр – это сообщение журнала, а третий позволяет вам решить, хотите ли вы продолжить (Continue) или прервать (Abort) тест в случае сбоя.

При запуске показанного выше теста в выходном журнале будет отображаться что-то вроде этого:

XFAIL  : TestIntBitset::testSetOff() isAnyOff not implemented yet
   Loc: [../../UnitTests/TestIntBitset/TestIntBitset.cpp(67)]

Пропуск теста

Если вы хотите пропустить тест или его часть, вы можете использовать макрос QSKIP, который пометит тест как пропущенный и остановит выполнение:

void TestIntBitset::testOperators()
{
    QSKIP("Operators have not been implemented yet...");
}

Никакой код после QSKIP выполняться не будет, но код перед ним – будет, поэтому, если какая-либо проверка завершится неудачей, тест будет считаться проваленным.

При выполнении пропущенного теста в журнале будет отображаться следующий текст:

SKIP : TestIntBitset::testOperators() Operators have not been implemented yet...
    Loc: [../../UnitTests/TestIntBitset/TestIntBitset.cpp(28)]

Решение об использовании QFAIL и QSKIP иногда может быть спорным. Для этого нет точного правила, и всё зависит от ваших задач проектирования. Лично я предпочитаю использовать QFAIL, когда заранее знаю, что что-то завершится неудачей, и хочу это выделить, а QSKIP, когда не имеет значения, выполнится ли весь тест или его часть.

Предупреждающие сообщения

Если вы хотите напечатать предупреждающее сообщение в журнале тестов, вы можете использовать QWARN. Этот макрос может быть полезен, когда вы хотите уведомить, что что-то в тесте идет не так, как ожидалось.

void TestIntBitset::testSetOff()
{
	mBS.setAllOn();

	unsigned int bitsOff = 0;
	mBS.setBitOff(BITS_IN_BYTE * bitsOff++);
	// ... еще тестовый код ...

	// этот тест будет вызывать предупреждение
	if((BITS_IN_BYTE * bitsOff) < BITS_IN_INT)
		QVERIFY(!mBS.isBitOff(BITS_IN_BYTE * bitsOff));
	else
		QWARN("trying to verify bit out of set bounds");

	// ... еще тестовый код ...
}

В этом случае проверка QVERIFY завершится неудачей, поскольку входные данные неверны. Было бы нечестно провалить тест из-за возможной ошибки в локальном коде, но эту ситуацию необходимо выделить. Предупреждение – хороший способ добиться этого.

При запуске теста, содержащего предупреждение, в выходном журнале будет отображаться что-то вроде этого:

WARNING: TestIntBitset::testSetOff() trying to verify value out of set bounds
   Loc: [../../UnitTests/TestIntBitset/TestIntBitset.cpp(75)]

В предупреждающем сообщении всегда будет само сообщение и то, где было выдано предупреждение.

Интеграция с Qt Creator

Неудивительно, что Qt Creator предлагает отличную интеграцию с Qt Test.

Одна из панелей слева называется «Тесты» (Tests) и отображает все юнит-тесты, найденные в вашем «родительском» проекте.

Рисунок 3 Панель тестов в Qt Creator
Рисунок 3 – Панель тестов в Qt Creator

С помощью этой панели вы можете отключить некоторые тесты, запустить их все или запустить только определенный тест. При проведении тестирования на основе данных вы также можете выбрать, какие наборы данных будут включены. Всё это чрезвычайно полезно в реальном проекте, где у вас могут быть сотни или даже тысячи юнит-тестов, и вам нужно проверить/отладить только один/несколько.

При запуске модульного теста с панели «Тесты» результаты отображаются на панели «Результаты тестирования» (Test Results), которую также можно открыть с помощью комбинации клавиш Alt + 8.

Рисунок 4 Панель результатов тестирования в Qt Creator
Рисунок 4 – Панель результатов тестирования в Qt Creator

Эта панель ясно покажет, какие тесты пройдены, а какие нет, а также покажет другую полезную информацию, особенно в случае сбоя. А именно нажмите на результаты, чтобы перейти к любой ошибке или предупреждению в коде. Кроме того, эта панель позволяет вам фильтровать события, которые вы хотите видеть в журнале, например, вы можете проверять только ошибки или предупреждения.

Комбинация этих двух панелей является отличным дополнением к Qt Creator и Qt Test и предлагает очень мощный инструмент.

Исходный код

Полный исходный код этого руководства доступен на GitHub.

Там вы найдете 3 проекта qmake, но для сборки вам нужно загрузить только первый уровень (UnitTests.pro).

Справочная информация

Чтобы узнать больше о Qt Test, вы можете ознакомиться с последней документацией по пространству имен QTest.

Заключение

Функции, обсуждаемые в данном руководстве, делают Qt Test более удобным и полным фреймворком, чем тот, что был представлен в первой части этой серии. В частности, интеграция с Qt Creator чрезвычайно полезна и эффективна. Тестирование, управляемое данными, также может быть очень мощным средством для сокращения кода тестирования и очень простого тестирования различных тестовых случаев.

Всё станет еще интереснее, когда в следующей статье я расскажу о тестировании графического интерфейса, так что следите за новостями.

Теги

C++ / CppQtQt CreatorQtTestМодульное тестирование / Юнит-тестирование / Unit testing

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.