Тестирование. Юнит-тесты.

Описание: Разработка и отладка приложений. Упор на 3D-графику.

dyvniy M
Автор темы, Администратор
Администратор
Аватара
dyvniy M
Автор темы, Администратор
Администратор
Возраст: 41
Репутация: 1
Лояльность: 1
Сообщения: 3579
Зарегистрирован: Ср, 10 октября 2012
С нами: 11 лет 6 месяцев
Профессия: Программист
Откуда: Россия, Москва
ICQ Сайт Skype ВКонтакте

#1 dyvniy » Вт, 24 ноября 2015, 11:43:09

JavaScript QUnit
http://habrahabr.ru/post/83170/
Спойлер
QUnit. Тестирование javascript кода
JavaScript*
Наткнулся вчера на этот инструмент и не смог пройти мимо, провел ночь за написанием тестов, а теперь хочу поделиться находкой. QUnit — это библиотека от разработчиков jQuery, позволяющая писать unit-тесты для кода на javascript. Удобна в использовании, ничего лишнего, осваивается за 20 минут, выгода от применения — колоссальная.

Самым нетерпеливым сразу ссылки:
Официальная документация на сайте jquery: docs.jquery.com/QUnit
Реальные примеры тестов (для модулей jquery): view.jquery.com/trunk/jquery/test/unit/
Руководство для начинающих (англ): www.swift-lizard.com/2009/11/24/test-driven-development-with-jquery-qunit/
Система распределенного тестирования (гениально и просто): testswarm.com/

Под катом информация о преимуществах юнит-тестирования применительно к js и разбор возможностей библиотеки на примерах.

Зачем писать unit-тесты?

Представим себе обычный цикл разработки: получили задачу, решили её, протестировали, починили баги и выпустили версию. Затем получили баг-репорты и фиче-реквесты и приступили к новому циклу разработки. По завершению этого цикла нам снова надо будет проверить, что всё то, что было реализовано ранее, по прежнему работает — провести регрессионное тестирование. И проводить его надо будет для каждого нового цикла разработки. По мере разрастания проекта на это будет уходить всё больше и больше времени. А как происходит регрессионное тестирование в web-проектах? Кликаем мышкой по кнопкам и ссылкам. В каждом браузере, для каждой фичи, на каждом цикле разработки. Нашли баг, поправили, обновляем страницу и снова кликаем, кликаем, кликаем.

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

Касательно web-разработки есть ещё одно огромное преимущество — запуск тестов под разными платформами и браузерами. Больше нет нужды проверять дотошно, как этот кусок кода будет работать в msie, понравится ли он опере, а как к нему отнесется сафари. Достаточно написать тест, который проверит функциональность. Более того, эту работу можно распределить между обычными пользователями, хороший пример такой функциональности — testswarm.com.
Как использовать QUnit

Это очень просто: понядобятся два файла:
QUnit.js и QUnit.css, а также новый html документ примерно такого содержания:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<link rel="stylesheet" href="qunit.css" type="text/css" media="screen">
<script type="text/javascript" src="qunit.js"></script>
<script type="text/javascript" src="your-code-for-testing.js"></script>
<script type="text/javascript" src="your-tests.js"></script>
</script>
</head>
<body>
<h1 id="qunit-header">QUnit test env</h1>
<h2 id="qunit-banner"></h2>
<h2 id="qunit-userAgent"></h2>
<ol id="qunit-tests">
</ol>
</body>
</html>

* This source code was highlighted with Source Code Highlighter.


Теперь подключаем свой код и можно писать тесты.

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

Это проще чем кажется. Давайте протестируем функцию trim, которая удаляет пробелы и табы на концах строки. Вот её код:
function trim(text) {
return (text || "").replace(/^\s+|\s+$/g, "");
}

* This source code was highlighted with Source Code Highlighter.


А вот так её можно протестировать:

test('trim()', function () {
equals(trim(''), '', 'Пустая строка');
ok(trim(' ') === '', 'Строка из пробельных символов');
same(trim(), '', 'Без параметра');

equals(trim(' x'), 'x', 'Начальные пробелы');
equals(trim('x '), 'x', 'Концевые пробелы');
equals(trim(' x '), 'x', 'Пробелы с обоих концов');
equals(trim(' x '), 'x', 'Табы');
equals(trim(' x y '), 'x y', 'Табы и пробелы внутри строки не трогаем');
});

* This source code was highlighted with Source Code Highlighter.


Разберем пример построчно. В первой строке вызов функции test. Первым параметром обозначаем функционал который тестируем. Последним — тестирующую функцию. Внутри этой функции производятся различные проверки. В данном случае мы проверяем соответствие результата выполнения функции и ожидаемой строки. Для проверки на соответствие используются функции:
equals — проверяет равенство первых двух параметров (нестрогая проверка, только для скалярных величин)
ok — истинность первого параметра
same — строгая проверка на равенство первых двух параметров (проверяет также равенство двух массивов и объектов)

Последним параметром функции принимают описание тестового случая.
В результате этой проверки получаем следующую картину:

Все тесты пройдены.

Как протестировать ajax? Асинхронные проверки.


С синхронными функциями просто. А что с асинхронными? Очевидно, для асинхронности мы должны остановить нормальный поток управления и по окончанию теста возобновить его. Этому служат функции stop() и start(). Вот простой пример:

test('async', function () {
// Останавливаем поток выполнения на этом тесте
stop();

setTimeout(function () {
ok(true);

// По завершению теста
// возобновляем работу тестировщика
start();
}, 500);
});

* This source code was highlighted with Source Code Highlighter.


Чтобы не вызывать всякий раз stop(); предусмотрен следующий вариант функции:

asyncTest('async', function () {
// поток остановлен автоматически

setTimeout(function () {
ok(true);

// Возобновляем конечно же вручную
start();
}, 500);
});

* This source code was highlighted with Source Code Highlighter.


А что если надо вызвать несколько асинхронных проверок в одном тесте? Когда в этом случае «стартовать» поток? Решение предлагается такое:

asyncTest('asynctest', function () {
// Pause the test
expect(3);

$.get(function () {
// асинхронные проверки
ok(true);
});

$.get(function () {
// другие асинхронные проверки
ok(true);
ok(true);
});

setTimeout(function () {
start();
}, 2000);
});

* This source code was highlighted with Source Code Highlighter.


Поток стартуется через 2 секунды. За это время должны пройти три проверки (вызов expect сообщает об этом тестирующей программе). Кстати, вместо вызова expect можно передавать второй числовой параметр функции test (asyncTest). Поведение будет идентичным предыдущему примеру.

Группировка тестов по модулям

QUnit позволяет сгруппировать тесты по модулям. Для этого достаточно вызвать функцию module('Название модуля или группы тестов') непосредственно перед вызовом тестов. Это удобно.

Резюме

Вот, в принципе и всё что нужно для того, чтобы начать тестировать свой код в автоматическом режиме. За дополнительной информацией обращаться сюда: docs.jquery.com/QUnit
Очень хорошие примеры тестов можно найти здесь (это тесты для core jquery).
Спасибо за внимание.
javascript, tdd, test driven development, qunit, testswarm, unit testing
+65

31,3к

336


Чаккаев Анатолий @1602 карма119,9 рейтинг0,0

Похожие публикации
+14
Real-life unit tests 3,5к 52 50
+12
Избирательное юнит-тестирование или ещё раз о тонких контроллерах 1,7к 34 11
+107
Анти-паттерны Test Driven Development 6,9к 174 64
Самое читаемое
Сейчас Неделя Месяц
Управление вещами реального мира из виртуального мира Minecraft (перевод) 10
Релиз Android Studio 2.0: две убер-фичи 10
Первый в мире плавучий дата-центр спустили на воду 16
Superfish: возвращение 16
Как мы проектируем и прототипируем всякую фигню 34
400 потрясающих бесплатных сервисов 29
Жизнь PHP-разработчика 234
Анализ резюме с HeadHunter. Кто сколько зарабатывает и в каких отраслях работает 55
Инфраструктура и торговые роботы: Какие языки программирования используются в сфере финансов 5
Декларативное программирование на C++ 2
Вопросы по теме
Как отсортировать массив объектов JS? 1
Как сделать подгрузку из сети в Cordova? 1
Каким JQuery плагином увеличит картинку до 95% экрана? 1
Пробки на дорогах, как можно сделать информер баллов? 1
Есть ли возможность сделать мобильное веб-приложение с уведомлениями? 1
Комментарии (22)

–6 neformal 5 февраля 2010 в 10:28#
Ну наконец то на Хабре опубликовали полезный пост.
Спасибо.
+2 rigid 5 февраля 2010 в 10:32#
спасибо за статью. в закладки одноззначно
+2 vovich 5 февраля 2010 в 10:41#
а где можно почитать побольше про юнит тесты? у меня возникла проблема с ними — на этапе разработки слишком много времени уходит на написание фикстур и т.д. Как правильно пользовать юнит тесты в условиях быстро меняющихся условий?
+2 etl 5 февраля 2010 в 10:46#↵↑
JS Ninja — как раз ничанается с концепции юниттестов.
0 Aralot 5 февраля 2010 в 11:20#↵↑
а скажите кто-нибудь внятно — она уже вышла? :)
+1 etl 5 февраля 2010 в 13:11#↵↑
Preorder)) www.manning.com/resig/
0 keith 5 февраля 2010 в 11:05#↵↑
хороший вопрос.
в процессе работы над очередной версией компонента/проекта имеет смысл писать тесты на неизменяемый функционал — отправка письма, доступ к данным и сервисам.
бизнес-логику, которая сильно подвержена изменениям лучше покрывать тестами выборочно и ближе к концу текущей версии.
если бизнес-логика сильно меняется от версии к версии, то тут надо решить что важнее — время разработки или стабильность релизов, хотя чаще всего можно найти компромис.
0 vovich 5 февраля 2010 в 11:19#↵↑
А есть ли смысл тратить время, покрывая юнит тестами мелкий функционал, особенно отправку писем. Это достаточно просто проверяется и врядли сломается. А вот как раз бизнес логика, где полно связаных компонентов, вот тут и возникают проблемы, с тем, что после изменений система работает, но с ошибкой. Из моего опыта чаще всего появляются баги:
1) верстка — тут уж только визуально, заодно и тестирование функционала
2)SQl скрипты — из за ошибки в скрипте берутся либо не те данные, либо не в правильном порядке, либо сохраняются нетуда, ХЗ как такое проверить и протестировать
3) Логика — чаще всего в операторах IF и switch. тут как раз, как я понимаю, и помогут юнит тесты.
4) плохие входные данные — тут опять же помогут юнит тесты.
+2 lomik 5 февраля 2010 в 12:39#↵↑
> 2)SQl скрипты — из за ошибки в скрипте берутся либо не те данные, либо не в правильном порядке, либо сохраняются нетуда, ХЗ как такое проверить и протестировать

А в чем проблема? При запуске тестов создается временная база данных, заполняется тестовыми данными и проверяется работоспособность. По окончании база грохается.
0 keith 5 февраля 2010 в 14:53#↵↑
я говорил не про мелкий функционал, а про неизменяемый. отправка письма в интернет-магазине является критичным функционалом и может отваливаться когда захочет.
про верстку говорить не буду — с ней все понятно.
про доступ к данным я упомянул — я создаю тестовую БД и на ней тестирую.
логику — ближе к концу итерации. только логика — это не if и switch ;)
валидация — само собой.
+2 lomik 5 февраля 2010 в 12:46#↵↑
Кстати по поводу правильного использования. Один из методов — разработка через тестирование. По ссылке все достаточно коротко и понятно написано.
0 1602 5 февраля 2010 в 22:35#↵↑
Да, согласен с Вами, писать тесты до самого функциональности — хорошая практика. К сожалению, не всегда получается применить в жизни.
0 lomik 5 февраля 2010 в 22:41#↵↑
Могу даже уточнить — практически никогда не получается. Сам применяю только когда пишу для себя — медленно, аккуратно и без меняющегося непрерывно ТЗ. Зато как потом работает и удобство по добавлению новых функций/исправлению багов выше всяких похвал.

Но даже когда не получается этот метод использовать хорошо о нем помнить. Всегда можно написать таким методом не весь код, а только «ядро» или потенциально проблемные участки.
0 maxkoryukov 6 февраля 2010 в 16:26#↵↑
Нужно стремиться «покрыть» код тестами как можно полнее. Такое покрытие дает большой плюс при развитии уже внедренного продукта. Например, создали мы библиотеку, внедрили ее в свой проект, раздали в бесплатное пользование=). Все заработало, люди пользуются. Со временем оптимизируем ее и запускаем тесты на оптимизированной библиотеке. Если тесты прошли — значит новую версию с большой долей уверенности можно внедрять. Если нет — высока вероятность, что расти будет багтрак.
0 sindrom 5 февраля 2010 в 12:27#
Отличный топик! Я сам хотел написать про qunit, но вы меня опередили :)
0 1602 5 февраля 2010 в 22:37#↵↑
Всегда можно написать лучше ;)
Читаю комменты и понимаю, что многие темы не раскрыты, например тестирование до разработки, либо тестирование в рабочем окружении.
+3 kmike 5 февраля 2010 в 12:32#
А мне больше jspec нравится (не путать с jsspec): github.com/visionmedia/jspec

Умеет все то же самое (а, вероятно, и больше, на qunit детально не смотрел: как там у него с загрузкой html-фикстур, иерархией тестов с общими подготовительными действиями?), + очень приятный синтаксис.

Если кого смутит, что там как будто тесты не на js нужно писать — это не так, там можно и на js писать, просто для сокращения количества писанины там препроцессор сделан.

Еще есть jsTestDriver ( code.google.com/p/js-test-driver/ ), штука позволяет запускать js-юнит-тесты (втч написанные на qunit) сразу в нескольких браузерах автоматом — из командной строки или по нажатии кнопки в плагине к эклипсу, и передавать результаты в браузер, в командную строку или эклипсовскому плагину. Причем запускать тесты можно даже на браузерах, которые расположены на другом компьютере. Это удобнее, чем в браузере смотреть, и еще более все автоматизируется. + Удивительно простая установка и настройка, все сразу работает. Только 1 минус — с mootools & prototype не работает ну никак, поэтому не использую)
0 1602 5 февраля 2010 в 22:43#↵↑
Спасибо за ссылки. Надо попробовать.
0 SeVit19 июля 2013 в 16:11#↵↑
jspec 404 теперь выдаёт (
0 wildmandnd 5 февраля 2010 в 14:22#
Также используем QUnit. У него есть странная, но похоже фундаментальная проблема — поскольку тесты запускаются в отдельном окружении, то часто глючат связи с плагинами. Чаще всего у нас проблемы с jQuery.Cookie — плагин не инициализируется в тестовом окружении. «Голые» же тесты — на ура, простенько и аккуратно.
+1 Iskin 5 февраля 2010 в 21:04#
При всём моём уважении к jQuery, JSpec мне нравится больше, хотя бы более красивой страницей вывода результатов :).
0 STEVER25 сентября 2012 в 20:13#
Вот тут в конце поста дан альтернативный вариант запуска теста асинхронной цепочки вызовов.
Изображение

dyvniy M
Автор темы, Администратор
Администратор
Аватара
dyvniy M
Автор темы, Администратор
Администратор
Возраст: 41
Репутация: 1
Лояльность: 1
Сообщения: 3579
Зарегистрирован: Ср, 10 октября 2012
С нами: 11 лет 6 месяцев
Профессия: Программист
Откуда: Россия, Москва
ICQ Сайт Skype ВКонтакте

#2 dyvniy » Пт, 16 сентября 2016, 16:21:33

TDD на Qt
Спойлер
http://itnotesblog.ru/note.php?id=161
Спойлер
Разработка через тестирование в Qt
13.07.2015
12:18

C++, Qt, QTestLib, QtTest, TDD, Модульные тесты, Разработка через тестирование
Введение
С концепцией разработки через тестирование (TDD - test-driven development) я познакомился по книгам Роберта Мартина:

Чистый код. Создание, анализ и рефакторинг;
Идеальный программист. Как стать профессионалом разработки ПО.
Это действительно хорошие книги, которые следует прочитать, если вы намереваетесь серьезно заниматься программированием или уже это делаете, но по какой-то причине обошли эти книги стороной. В какой-то мере отношение к TDD у Роберта М. доведено до фанатизма. Однако нельзя не согласиться, что преимущества от применения этой техники весьма существенны. Но есть и свои недостатки, которые во многом связаны с ограничениями разработки через тестирование. Об этих преимуществах и недостатках в контексте использования Qt мы и поговорим.

Коротко о TDD
Идея создания программ с помощью TDD достаточно проста. Ее можно описать с помощью следующего алгоритма:

Создайте тест, который не будет проходить;
Напишите минимальное количество кода, который заставит тест проходить. Этот пункт самый важный и к нему сложнее всего привыкнуть. Более подробно мы обсудим его при рассмотрении примера;
Проведите рефакторинг (то есть реорганизуйте структуру кода для его улучшения без изменения функциональности), если это возможно;
Повторяйте шаги 1-3 до тех пор, пока разрабатываемая программа не будет делать то, что нужно :)
Звучит достаточно легко, но нужно приобрести некую сноровку, чтобы пользоваться этим приемом правильно.

Преимущества TDD
Перечислим главные преимущества, которые вы получите от использования TDD:

При строгом соблюдении весь код окажется протестированным. Поскольку на каждой итерации разработки новую логику и функционал мы добавляем лишь после того, как напишем соответствующий тест, то покрытие кода окажется 100%-ым;
Получившийся код будет иметь достаточно простую структуру. Это связано с тем, что мы просто не сможем нормально протестировать неудачно построенный модуль. Если мы сначала будем писать код, то вполне вероятно, что появится много зависимостей, из-за которых модульное тестирование может выйти либо неоправданно сложным, либо вообще практически невозможным. TDD же исключает этот вариант, потому что код пишется на основе тестов, а не наоборот;
Сюда же следует отнести все преимущества от применения модульного тестирования в целом. Это просто следствие, но оно оказывает не менее серьезное влияние на процесс разработки. Например, бонусом к TDD вы получаете мощный инструмент регрессионного тестирования. С ним вы всегда можете быть уверенны, что внесенные изменения не нарушили работу уже написанного кода, а если что-то и произошло, то вы сразу узнаете об этом по тестам, которые не прошли.
Недостатки TDD
Очень часто приверженцы TDD умалчивают о его недостатках, но они есть. И с ними нужно считаться. Вот наиболее важные из них:

Не для любого кода можно написать модульные тесты. Например, графический пользовательский интерфейс. Если речь идет о разработке виджета, то вполне можно разработать модульные тесты для проверки его функциональных возможностей. Но как вы скажете, что виджет выглядит правильно или нет? Конечно, можно задуматься о применении систем распознавания образов и искусственного интеллекта, но гораздо дешевле и результативнее проводить юзабилити тестирование с потенциальными пользователями. А как вы будете тестировать функцию Random()? Подумайте об этом в свободное время;
Тестирование не гарантирует правильность. Хорошее покрытие кода тестами не обеспечивает его корректность. Это означает лишь то, что код соответствует тем ожиданиям, которые вы учли при разработке тестов. Если же вы что-то упустили или поняли требования не так, то код может оказаться некорректным, причем вместе с тестами для него;
На сопровождение самих тестов уходит не мало времени. В случае строгого применения TDD вы не имеете права вносить изменения в основной код без добавления соответствующего теста. Это означает, что тестов будет много, и они сами могут стать источником ошибок. То есть вам придется заниматься не только отладкой и рефакторингом кода приложения, но и кода тестов, что может оказаться не менее трудным.
Пример использования TDD с Qt
А теперь попробуем создать несложный Qt-модуль с помощью TDD.

Прежде чем начать
Если вы поищите примеры использования TDD в интернете или литературе, то увидите, что практически всегда они сводятся к разработке модулей без внешних зависимостей. И в какой-то мере такая ситуация вполне оправдана, поскольку TDD для этого и предназначен (мое личное мнение, хотя вы можете быть не согласны). Например, с помощью TDD легко можно разработать алгоритм разложения целого числа на множители. Вполне понятно, как должна выглядеть соответствующая функция, какие она должна принимать аргументы, и какие тесты для ее проверки понадобятся.

А что вы скажете о разработке сетевого модуля? Или модуля для взаимодействия с базами данных? В этом случае мы имеет внешние зависимости, контроль над которыми ограничен. В большинстве случаев мы не можем воспроизвести редкие ошибки сторонних систем по нашему желанию. И как же поступить? - Для этого используют Mock-объекты. Идея заключается в том, что для тестирования можно использовать не настоящую внешнюю систему, а некую фиктивную реализацию. В простейшем случае такой Mock-объект может возвращать константу. В других же случаях он может использовать упрощенную реализацию функционала реальной подсистемы (например, сохранять данные не в БД, а в ассоциативном массиве в памяти). Еще существуют разновидности Mock-фреймворков, которые позволяют создавать Mock-объекты с произвольными последовательностями возвращаемых значений (например, вас может заинтересовать проект googlemock).

Как я уже отмечал, мое мнение заключается в том, что TDD эффективнее применять для разработки систем без внешних зависимостей. Конечно, Mock-объекты - это выход, но так ли они полезны? С одной стороны, если задуматься о принципе DRY (см. Принцип DRY в действии), то смысл есть. Ведь если вы выявили какую-то ошибку в коде взаимодействия с БД, то вполне вероятно, что она когда-нибудь может повториться вновь. В этом случае у вас три варианта: либо тестировать ее вручную после каждого изменения; либо надеяться, что она больше никогда не произойдет; либо автоматизировать ее проверку с помощью соответствующих модульных тестов. Первый вариант плох тем, что вы будете делать лишнюю работу, то есть повторяться. Второй вариант делает ваш код уязвимым. Последний же вариант потребует довольно много дополнительной работы. На мой взгляд, в этом случае все зависит от масштабов и критичности проекта. Если ошибки недопустимы (например, авиационное или медицинское ПО), то тестирование обязано быть исчерпывающим, а поэтому автоматизированным. С другой стороны, если вы пишите приложение для среднестатистического пользователя, то вполне можете рассчитывать на некий уровень толерантности. Вспомните те же продукты из серии Microsoft Office. Не знаю как у вас, но у меня они регулярно падают и глючат (при этом я пользуюсь лицензионными версиями).

А теперь к делу
Давайте реализуем с помощью TDD алгоритм для поворота матрицы на 90 градусов по часовой стрелке. Он должен делать примерно следующее:

matrix_rotation

То есть можно представить, что берется некая матрица и просто переворачивается на бок. Выглядит достаточно легко, поэтому думаю, что мы справимся.

И первым делом подготовим Qt-проект со следующей структурой:

.
├── app.pri
├── bin/
├── common.pri
├── include/
│ ├── tdddemolib_global.h
│ └── tdddemolib.h
├── lib.linux/
├── lib.pri
├── src/
│ └── TDDDemoLib/
│ ├── tdddemolib.cpp
│ └── TDDDemoLib.pro
├── TDDDemo.pro
├── TDDDemo.pro.user
└── tests/
└── TDDDemoTest/
├── TDDDemoTest.pro
└── tst_tdddemotest.cpp
Как вы можете видеть, проект состоит из двух модулей: TDDDemoLib и TDDDemoTest. Как понятно из названия, первый модуль представляет собой библиотеку, в которой мы определим и реализуем нашу функцию, а во второй модуль мы поместим наши тесты. Более подробно про организацию Qt-проектов вы можете прочитать в моей заметке Структура Qt-проекта на C++, поэтому здесь я лишь приведу содержимое pro-файлов без дополнительных комментариев.

TDDemo.pro:

TEMPLATE = subdirs

SUBDIRS += \
src/TDDDemoLib \
tests/TDDDemoTest
TDDemoLib.pro:

QT -= gui

include( ../../common.pri )
include( ../../lib.pri )

TARGET = TDDDemoLib$${LIB_SUFFIX}
TEMPLATE = lib

DEFINES += TDDDEMOLIB_LIBRARY

SOURCES += tdddemolib.cpp

HEADERS += ../../include/tdddemolib.h\
../../include/tdddemolib_global.h
TDDemoTest.pro:

QT += testlib

QT -= gui

TARGET = tst_tdddemotest
CONFIG += console
CONFIG -= app_bundle

TEMPLATE = app


SOURCES += tst_tdddemotest.cpp

include( ../../common.pri )
include( ../../app.pri )

LIBS += -lTDDDemoLib$${LIB_SUFFIX}
Здесь мы лишь обратим внимание на то, что в подпроекте TDDDemoTest мы подключили Qt-модуль testlib. С его помощью мы и будем проводить тестирование.

В tdddemolib.h сейчас мы определим лишь тип матрицы:

#ifndef TDDDEMOLIB_H
#define TDDDEMOLIB_H

#include "tdddemolib_global.h"

#include <QMetaType>
#include <QVector>

typedef QVector< QVector< int > > Matrix;

Q_DECLARE_METATYPE( Matrix )

#endif // TDDDEMOLIB_H
Обратите внимание, что в этом фрагменте мы воспользовались Q_DECLARE_METATYPE. Это сделано для того, чтобы в коде тестов задействовать одну весьма удобную возможность, о который мы скоро поговорим.

Но раз используем TDD, то в первую очередь займемся тестами. Посмотрим на содержимое tst_tdddemotest.cpp:

#include <QString>
#include <QtTest>

#include <tdddemolib.h>

class TDDDemoTest : public QObject {
Q_OBJECT

public:
TDDDemoTest();

private slots:
void rotate90DegreesTest();
void rotate90DegreesTest_data();
};

TDDDemoTest::TDDDemoTest() {
}

void TDDDemoTest::rotate90DegreesTest() {
QFETCH( Matrix, matrix );
QFETCH( Matrix, result );
}

void TDDDemoTest::rotate90DegreesTest_data() {
QTest::addColumn< Matrix >( "matrix" );
QTest::addColumn< Matrix >( "result" );
}

QTEST_APPLESS_MAIN( TDDDemoTest )

#include "tst_tdddemotest.moc"
Для организации наших тестов мы будем использовать класс TDDDemoTest. Он наследует QObject и имеет два слота: rotate90DegreesTest() и rotate90DegreesTest_data(). Это позволит нам строить тесты, управляемые данными. То есть мы всего один раз определим процедуру проверки в rotate90DegreesTest(), а все данные будем компоновать в соответствующем слоте rotate90DegreesTest_data(). Определение структур данных, которые будут использоваться в тесте, осуществляется с помощью QTest::addColumn(). А получение этих данных выполняется с помощью макроса QFETCH. По сути этот механизм работает на основе QVariant, поэтому нам и понадобилось объявление мета-типа для Matrix.

Модульный тест в Qt является исполняемым приложением (в нашем случае консольным), поэтому в нем предусмотрен макрос для определения функции main(). Этим макросом мы и воспользовались: QTEST_APPLESS_MAIN( TDDDemoTest ). Кроме того, обратите внимание на последнюю строку. Поскольку весь код теста мы пишем в одном cpp-файле, то нам нужно явно указать инструкцию #include "tst_tdddemotest.moc". Это нужно лишь из-за того, что мы унаследовали класс QObject и использовали макрос Q_OBJECT для получения возможности добавления слотов.

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

void TDDDemoTest::rotate90DegreesTest() {
QFETCH( Matrix, matrix );
QFETCH( Matrix, result );

QCOMPARE( rotate90Degrees( matrix ), result );
}
Это тот же слот, который мы уже рассматривали. Но в него добавлена одна строка. В ней мы используем макрос QCOMPARE. Он обеспечивает сравнение фактического и ожидаемого результатов работы функции на основе переданных данных. Если они будут отличаться, то тест будет засчитан как не пройденный.

Теперь добавим первые тестовые данные:

void TDDDemoTest::rotate90DegreesTest_data() {
QTest::addColumn< Matrix >( "matrix" );
QTest::addColumn< Matrix >( "result" );

QTest::newRow( "Empty matrix" ) << Matrix { { } } << Matrix { { } };
}
Мы делаем это с помощью QTest::newRow(). В качестве простейшего теста мы выполняем поворот пустой матрицы. Для краткости записи я использовал синтаксис C++11 при инициализации матриц (то есть векторов). Конечно, можно обойтись и без них, но тогда кода вышло бы существенно больше.

Отлично. Первый тест готов. Но мы не можем даже собрать его, поскольку у нас еще нет функции rotate90Degrees(). Добавим ее объявление и минимальную реализацию:

// Добавили в tdddemolib.h:
Matrix rotate90Degrees( const Matrix& matrix );

// Добавили в tdddemolib.cpp:
Matrix rotate90Degrees( const Matrix& matrix ) {
return matrix;
}
И вот что мы можем увидеть на консоли после запуска теста:

********* Start testing of TDDDemoTest *********
Config: Using QTest library 4.8.6, Qt 4.8.6
PASS : TDDDemoTest::initTestCase()
PASS : TDDDemoTest::rotate90DegreesTest()
PASS : TDDDemoTest::cleanupTestCase()
Totals: 3 passed, 0 failed, 0 skipped
********* Finished testing of TDDDemoTest *********
Прекрасно. Пора добавлять новые тестовые данные:

void TDDDemoTest::rotate90DegreesTest_data() {
// ...

QTest::newRow( "2x1 single value matrix" ) <<
Matrix { { 0, 0 } } <<
Matrix { { 0 },
{ 0 } };
}
Этот вариант соответствует случаю матрицы, состоящей из одной строки, включающей два одинаковых символа. В результате должна получиться матрица-столбец. Попробуем запустить тест:

********* Start testing of TDDDemoTest *********
Config: Using QTest library 4.8.6, Qt 4.8.6
PASS : TDDDemoTest::initTestCase()
FAIL! : TDDDemoTest::rotate90DegreesTest(2x1 single value matrix) Compared values are not the same
Loc: [tst_tdddemotest.cpp(24)]
PASS : TDDDemoTest::cleanupTestCase()
Totals: 2 passed, 1 failed, 0 skipped
********* Finished testing of TDDDemoTest *********
Тест не прошел. Нужно это исправить:

Matrix rotate90Degrees( const Matrix& matrix ) {
if( matrix[ 0 ].isEmpty() ) {
return matrix;
}

return Matrix { { 0 },
{ 0 } };
}
Если вы не знакомы с TDD, то представленный промежуточный алгоритм может показаться вам необычным. Но в этом и заключается идея разработки через тестирование. Не усложняйте реализацию раньше времени. Пишите минимальное количество кода, которое способно обеспечить прохождение всех имеющихся к текущему моменту тестов. Убедимся, что это так, и тесты проходят:

********* Start testing of TDDDemoTest *********
Config: Using QTest library 4.8.6, Qt 4.8.6
PASS : TDDDemoTest::initTestCase()
PASS : TDDDemoTest::rotate90DegreesTest()
PASS : TDDDemoTest::cleanupTestCase()
Totals: 3 passed, 0 failed, 0 skipped
********* Finished testing of TDDDemoTest *********
Отлично. Добавляем новый набор данных:

void TDDDemoTest::rotate90DegreesTest_data() {
// ...

QTest::newRow( "2x1 different values matrix" ) <<
Matrix { { 1, 2 } } <<
Matrix { { 1 },
{ 2 } };
}
Теперь у нас матрица 2x1, состоящая из разных элементов. Тест провален. Исправляем код:

Matrix rotate90Degrees( const Matrix& matrix ) {
if( matrix[ 0 ].isEmpty() ) {
return matrix;
}

return Matrix { { matrix[ 0 ][ 0 ] },
{ matrix[ 0 ][ 1 ] } };
}
Теперь тест проходит. Пора добавить новые тестовые данные:

void TDDDemoTest::rotate90DegreesTest_data() {
// ...

QTest::newRow( "1x2 different values matrix" ) <<
Matrix { { 1 },
{ 2 } } <<
Matrix { { 2, 1 } };
Теперь в тестовых данных мы используем матрицу 1x2 с разными элементами. Тест вновь не проходит, сообщая об ошибке "index out of range". Займемся доработкой функции:

Matrix rotate90Degrees( const Matrix& matrix ) {
if( matrix[ 0 ].isEmpty() ) {
return matrix;
} else if( matrix[ 0 ].size() == 2 ) {
return Matrix { { matrix[ 0 ][ 0 ] },
{ matrix[ 0 ][ 1 ] } };
}

return Matrix { { matrix[ 1 ][ 0 ], matrix[ 0 ][ 0 ] } };
}
Пока что реализация выглядит весьма неуклюже, но тест проходит. Большего нам пока что и не требуется. Пора писать новый набор тестовых данных:

void TDDDemoTest::rotate90DegreesTest_data() {
// ...

QTest::newRow( "2x2 different values matrix" ) <<
Matrix { { 1, 2 },
{ 3, 4 } } <<
Matrix { { 3, 1 },
{ 4, 2 } };
}
Теперь мы имеем дело с матрицей 2x2 из разных элементов. Разумеется, тест не проходит. Исправим это:

Matrix rotate90Degrees( const Matrix& matrix ) {
if( matrix[ 0 ].isEmpty() ) {
return matrix;
} else if( matrix[ 0 ].size() == 2 ) {
if( matrix.size() == 2 ) {
return Matrix { { matrix[ 1 ][ 0 ], matrix[ 0 ][ 0 ] },
{ matrix[ 1 ][ 1 ], matrix[ 0 ][ 1 ] } };
}
return Matrix { { matrix[ 0 ][ 0 ] },
{ matrix[ 0 ][ 1 ] } };
}

return Matrix { { matrix[ 1 ][ 0 ], matrix[ 0 ][ 0 ] } };
}
Опять мы использовали самое прямое решение возникшей проблемы. Однако оно работает и теперь тест проходит. Пора снова все испортить и добавить еще один набор тестовых данных:

void TDDDemoTest::rotate90DegreesTest_data() {
// ...

QTest::newRow( "3x1 different values matrix" ) <<
Matrix { { 1, 2, 3 } } <<
Matrix { { 1 },
{ 2 },
{ 3 } };
}
В этот раз мы взяли матрицу большей размерности с разными элементами. Наша текущая реализация функции с этим не справится. Требуются доработки:

Matrix rotate90Degrees( const Matrix& matrix ) {
if( matrix[ 0 ].isEmpty() ) {
return matrix;
} else if( matrix[ 0 ].size() == 2 ) {
if( matrix.size() == 2 ) {
return Matrix { { matrix[ 1 ][ 0 ], matrix[ 0 ][ 0 ] },
{ matrix[ 1 ][ 1 ], matrix[ 0 ][ 1 ] } };
}
return Matrix { { matrix[ 0 ][ 0 ] },
{ matrix[ 0 ][ 1 ] } };
} else if( matrix[ 0 ].size() == 3 ) {
return Matrix { { matrix[ 0 ][ 0 ] },
{ matrix[ 0 ][ 1 ] },
{ matrix[ 0 ][ 2 ] } };
}

return Matrix { { matrix[ 1 ][ 0 ], matrix[ 0 ][ 0 ] } };
}
Необходимость в рефакторинге уже достаточно очевидна. Но давайте сначала добавим еще один набор тестовых данных:

void TDDDemoTest::rotate90DegreesTest_data() {
// ...

QTest::newRow( "3x2 different values matrix" ) <<
Matrix { { 1, 2, 3 },
{ 4, 5, 6 } } <<
Matrix { { 4, 1 },
{ 5, 2 },
{ 6, 3 } };
}
Мы опять увеличили размер матрицы, добавив к предыдущему случаю еще один ряд. Тест не проходит. Вновь беремся за код функции:

Matrix rotate90Degrees( const Matrix& matrix ) {
if( matrix[ 0 ].isEmpty() ) {
return matrix;
} else if( matrix[ 0 ].size() == 2 ) {
if( matrix.size() == 2 ) {
return Matrix { { matrix[ 1 ][ 0 ], matrix[ 0 ][ 0 ] },
{ matrix[ 1 ][ 1 ], matrix[ 0 ][ 1 ] } };
}
return Matrix { { matrix[ 0 ][ 0 ] },
{ matrix[ 0 ][ 1 ] } };
} else if( matrix[ 0 ].size() == 3 ) {
if( matrix.size() == 2 ) {
return Matrix { { matrix[ 1 ][ 0 ], matrix[ 0 ][ 0 ] },
{ matrix[ 1 ][ 1 ], matrix[ 0 ][ 1 ] },
{ matrix[ 1 ][ 2 ], matrix[ 0 ][ 2 ] } };
}

return Matrix { { matrix[ 0 ][ 0 ] },
{ matrix[ 0 ][ 1 ] },
{ matrix[ 0 ][ 2 ] } };
}

return Matrix { { matrix[ 1 ][ 0 ], matrix[ 0 ][ 0 ] } };
}
Теперь тесты проходят, но закономерности настолько очевидны, что структуру кода без особых сложностей можно упростить. Давайте сделаем это. Но начнем проводить изменения постепенно:

Matrix rotate90Degrees( const Matrix& matrix ) {
const int rowCount = matrix[ 0 ].size();
const int columnCount = matrix.size();

if( rowCount == 0 ) {
return matrix;
} else if( rowCount == 2 ) {
if( columnCount == 2 ) {
return Matrix { { matrix[ 1 ][ 0 ], matrix[ 0 ][ 0 ] },
{ matrix[ 1 ][ 1 ], matrix[ 0 ][ 1 ] } };
}
return Matrix { { matrix[ 0 ][ 0 ] },
{ matrix[ 0 ][ 1 ] } };
} else if( rowCount == 3 ) {
if( columnCount == 2 ) {
return Matrix { { matrix[ 1 ][ 0 ], matrix[ 0 ][ 0 ] },
{ matrix[ 1 ][ 1 ], matrix[ 0 ][ 1 ] },
{ matrix[ 1 ][ 2 ], matrix[ 0 ][ 2 ] } };
}

return Matrix { { matrix[ 0 ][ 0 ] },
{ matrix[ 0 ][ 1 ] },
{ matrix[ 0 ][ 2 ] } };
}

return Matrix { { matrix[ 1 ][ 0 ], matrix[ 0 ][ 0 ] } };
}
В приведенном коде мы просто заметили, что размеры матрицы, которую нужно перевернуть, используются многократно, поэтому определили соответствующие константы rowCount и columnCount. Обратите внимание, что они определены для перевернутой, а не исходной матрицы. То есть количество строк стало количеством столбцов и наоборот.

Теперь обратим внимание, что во всех случаях (кроме rowCount == 0) на i-ой строке в j-ом столбце перевернутой матрицы стоит элемент исходной матрицы вида: matrix[ matrix.size() - 1 - j ][ i ]. Проведем соответствующий рефакторинг:

Matrix rotate90Degrees( const Matrix& matrix ) {
const int rowCount = matrix[ 0 ].size();
const int columnCount = matrix.size();

if( rowCount == 0 ) {
return matrix;
}

Matrix rotatedMatrix( rowCount );
for( int i = 0; i < rowCount; ++i ) {
rotatedMatrix[ i ].resize( columnCount );
for( int j = 0; j < columnCount; ++j ) {
rotatedMatrix[ i ][ j ] = matrix[ columnCount - 1 - j ][ i ];
}
}
return rotatedMatrix;
}
Еще раз запустим тесты и убедимся, что ничего не сломали. Отлично. Все работает. Но мы еще не закончили. Ведь до сих пор мы рассматривали лишь корректные тестовые данные. Теперь необходимо написать тесты для случаев ошибочного использования нашей функции. Добавим в тестовый класс еще два слота:

void rotate90DegreesExceptionTest();
void rotate90DegreesExceptionTest_data();
Организуем их по тому же принципу, что был использован при компоновке тестов на основе допустимых данных. Слот для проверки выполнения условия реализуем следующим образом:

void TDDDemoTest::rotate90DegreesExceptionTest() {
QFETCH( Matrix, matrix );
QFETCH( QString, errorMsg );

try {
rotate90Degrees( matrix );
QFAIL( "Must not reach this place" );
} catch( const std::invalid_argument& e ) {
QCOMPARE( QString::fromStdString( e.what() ), errorMsg );
}
}
В данном случае мы ожидаем, что строка QFAIL( "Must not reach this place" ) не должна достигаться, поскольку произойдет исключение std::invalid_argument и мы перейдем в соответствующий обработчик, где проверим полученное сообщение об ошибке. Добавим первые тестовые данные:

void TDDDemoTest::rotate90DegreesExceptionTest_data() {
QTest::addColumn< Matrix >( "matrix" );
QTest::addColumn< QString >( "errorMsg" );

QTest::newRow( "Matrix with no columns" ) << Matrix { } << "Matrix must have at least one column";
}
В качестве первого тестового набора данных мы передаем матрицу без столбцов. В этом случае тест не только не проходит, но и завершается с ошибкой выхода за границы массива. Опять возвращаемся к нашей реализации:

Matrix rotate90Degrees( const Matrix& matrix ) {
const int columnCount = matrix.size();
if( columnCount == 0 ) {
throw std::invalid_argument( "Matrix must have at least one column" );
}

const int rowCount = matrix[ 0 ].size();

if( rowCount == 0 ) {
return matrix;
}

Matrix rotatedMatrix( rowCount );
for( int i = 0; i < rowCount; ++i ) {
rotatedMatrix[ i ].resize( columnCount );
for( int j = 0; j < columnCount; ++j ) {
rotatedMatrix[ i ][ j ] = matrix[ columnCount - 1 - j ][ i ];
}
}
return rotatedMatrix;
}
Тест проходит. Добавим еще тестовых данных:

void TDDDemoTest::rotate90DegreesExceptionTest_data() {
// ...

QTest::newRow( "Matrix with rows of different sizes" ) << Matrix { { 1 },
{ 2, 3 } } <<
"Matrix must have the rows of the same size";
}
В этом случае мы хотим, чтобы в случае получения матрицы со строками разных размеров наша функция возвращала исключение. Убедимся, что тест не проходит:

********* Start testing of TDDDemoTest *********
Config: Using QTest library 4.8.6, Qt 4.8.6
PASS : TDDDemoTest::initTestCase()
PASS : TDDDemoTest::rotate90DegreesTest()
FAIL! : TDDDemoTest::rotate90DegreesExceptionTest(Matrix with rows of different sizes) Must not reach this place
Loc: [tst_tdddemotest.cpp(77)]
PASS : TDDDemoTest::cleanupTestCase()
Totals: 3 passed, 1 failed, 0 skipped
********* Finished testing of TDDDemoTest *********
Внесем поправки в реализацию нашей функции:

Matrix rotate90Degrees( const Matrix& matrix ) {
const int columnCount = matrix.size();
if( columnCount == 0 ) {
throw std::invalid_argument( "Matrix must have at least one column" );
}

const int rowCount = matrix[ 0 ].size();

if( rowCount == 0 ) {
return matrix;
}

Matrix rotatedMatrix( rowCount );
for( int i = 0; i < rowCount; ++i ) {
rotatedMatrix[ i ].resize( columnCount );
for( int j = 0; j < columnCount; ++j ) {
if( matrix[ columnCount - 1 - j ].size() != rowCount ) {
throw std::invalid_argument( "Matrix must have the rows of the same size" );
}
rotatedMatrix[ i ][ j ] = matrix[ columnCount - 1 - j ][ i ];
}
}
return rotatedMatrix;
}
Здесь мы просто добавили соответствующую проверку, поэтому если во входной матрице обнаружится строка другого размера, то будет выброшено исключение. Запустим тесты еще раз:

********* Start testing of TDDDemoTest *********
Config: Using QTest library 4.8.6, Qt 4.8.6
PASS : TDDDemoTest::initTestCase()
PASS : TDDDemoTest::rotate90DegreesTest()
PASS : TDDDemoTest::rotate90DegreesExceptionTest()
PASS : TDDDemoTest::cleanupTestCase()
Totals: 4 passed, 0 failed, 0 skipped
********* Finished testing of TDDDemoTest *********
Вот теперь можно считать, что мы закончили с реализацией функции поворота матрицы на 90 градусов по часовой стрелке. Но у вас мог возникнуть вопрос: "А зачем было все так усложнять, ведь можно было реализовать алгоритм сразу?". В какой-то мере я с вами согласен. С другой стороны, поскольку мы добавляли тесты постепенно, то в качестве бонуса получили следующее:

Набор регрессионных тестов, покрывающих все основные классы эквивалентности. То есть на каждый принципиально различный случай у нас есть тесты, которые мы можем запускать после любого изменения кода реализации. Конечно, мы бы могли продумать все тесты и без этого, но с помощью TDD мы пришли к получившемуся набору естественным путем. При этом у нас нет лишних тестов, поскольку на каждом шаге мы добавляли только такие тестовые данные, которые точно не проходят;
Мы получили алгоритм не сразу, а по частям. А это всегда проще. То есть мы посмотрели, что будет происходить для частных случаев. Выявили для них закономерности. И лишь затем обобщили алгоритм. Возможно, рассмотренная функция для поворота матрицы не настолько сложна, чтобы это было существенным преимуществом. Но в более запутанных случаях охватить сразу всю логику работы алгоритма может оказаться проблематичным. Поэтому вы в любом случае будете рассматривать некие варианты входных наборов параметров. Так почему бы заодно не автоматизировать их проверку?
Материала получилось достаточно много, поэтому думаю, что на этом мы сейчас и закончим. Однако не могу не упомянуть о том, что в QTestLib входит довольно много полезных инструментов. Например, с помощью QSignalSpy вы можете тестировать сигналы; для оценки производительности вы можете использовать QBENCHMARK; кроме того, не забывайте о возможностях эмуляции действий пользователя (щелчки мышью и нажатие клавиш), чтобы иметь возможность организовать тестирование виджетов.

Заключение
В качестве вывода могу сказать, что TDD имеет свои преимущества. Но лично для меня недостатки все же весьма ощутимы, чтобы не применять его в своей работе постоянно. Конечно, иногда удобно таким способом написать и отладить какой-то сложный алгоритм. Но я считаю, что для более тривиальных случаев это уже избыточно. К тому же, код взаимодействия с оборудованием, сетью или БД писать через тестирование довольно накладно. Приходится задумываться о подставных объектах и прочих побочных элементах. Таким образом, модульные тесты полезны, но все должно быть в меру и по назначению. Главное - выбрать правильный момент и инструмент для реализации ваших задумок.
Тут есть под проекты Qt)

Код: Выделить всё

//TDDemo.pro:

TEMPLATE = subdirs

SUBDIRS 
+= \
    src/TDDDemoLib \
    tests/TDDDemoTest

//TDDemoLib.pro:

QT       -= gui

include( ../../common.pri )
include(
 ../../lib.pri )

TARGET = TDDDemoLib$${LIB_SUFFIX}
TEMPLATE = lib

DEFINES 
+= TDDDEMOLIB_LIBRARY

SOURCES 
+= tdddemolib.cpp

HEADERS 
+= ../../include/tdddemolib.h\
        ../../include/tdddemolib_global.h

//TDDemoTest.pro:

QT       += testlib

QT       
-= gui

TARGET 
= tst_tdddemotest
CONFIG   
+= console
CONFIG   
-= app_bundle

TEMPLATE 
= app


SOURCES 
+= tst_tdddemotest.cpp

include( ../../common.pri )
include(
 ../../app.pri )

LIBS += -lTDDDemoLib$${LIB_SUFFIX}

http://www.prog.org.ru/topic_26413_0.html
Изображение


Название раздела: Программирование (под Desktop и Android)
Описание: Разработка и отладка приложений. Упор на 3D-графику.

Быстрый ответ


Введите код в точности так, как вы его видите. Регистр символов не имеет значения.
Код подтверждения
:) ;) :hihi: :P :hah: :haha: :angel: :( :st: :_( :cool: 8-| :beee: :ham: :rrr: :grr: :* :secret: :stupid: :music: Ещё смайлики…
   

Вернуться в «Программирование (под Desktop и Android)»

Кто сейчас на форуме (по активности за 15 минут)

Сейчас этот раздел просматривают: 126 гостей