PHPUnit. Автоматические тесты

http://www.phpunit.de/manual/3.4/en/automating-tests.html
  • Перевод
Предисловие переводчика:
Недавно начал изучать PHPUnit (framework семейства xUnit) и с удивлением обнаружил, что на русском языке нет статей про автоматические тесты для самых-самых чайников.
В первой главе документации по PHPUnit на примерах очень доступно рассказывается, что такое автоматическое тестирование.


Даже хорошие программисты допускают ошибки. Разница между хорошим программистом и плохим заключается в том, что хороший программист как можно чаще использует тесты, чтобы найти свои ошибки.
Чем раньше Вы начнете тестировать, тем выше Ваши шансы найти ошибку, и тем ниже цена исправления.
Это объясняет, почему откладывание тестирования до момента передачи программы заказчику является очень плохой практикой. Большинство ошибок так и не будет найдено, а цена исправления станет такой высокой, что Вам придется составить большой график работы, т.к. сразу Вы не сможете их все исправить.

Тестирование при помощь PHPUnit принципиально не отличается от того тестирования, которое Вы уже выполняете. Это просто другой подход к работе. Разница заключается в следующем: в одном случае Вы просто проверяете, что Ваша программа работает как ожидается, в другом — Вы выполняете серию тестов, которые представляют собой выполняемые фрагменты кода для автоматической проверки корректности частей (модулей) программного обеспечения.
Эти выполняемые фрагменты кода называются модульными (Unit) тестами.

В этой статье мы проделаем путь от элементарного теста, который просто выводит на экран результат своей работы до полностью автоматизированного теста.
Допустим, нас попросили протестировать встроенный в PHP массив (array). На одном из этапов тестирования надо проверить работу функции count(). Мы ожидаем, что для только что созданного массива функция count() вернет 0. После добавления элемента в массив count() должна вернуть 1.
Пример 1.1 демонстрирует, что мы хотим проверить.

Пример 1.1: Тестирование операторов массива
<?php<br>$fixture = array();<br>
// $fixture is expected to be empty.<br> <br>$fixture[] = 'element';<br>
// $fixture is expected to contain one element.<br>?>
<br><br>
* This source code was highlighted with Source Code Highlighter.


Самый простой способ проверить, получили ли мы то, что хотели — вывести результат работы функции count() на экран.
Вывод надо сделать до и после добавления элемента, смотрите Пример 1.2.
Если мы сначала получили 0 а потом 1, то array и count() работают как надо.

Пример 1.2: Использование вывода на экран для проверки операторов массива
<?php<br>$fixture = array();<br>
print count($fixture) . "\n";<br> <br>$fixture[] = 'element';<br>
print count($fixture) . "\n";<br>?>
<br><br>
* This source code was highlighted with Source Code Highlighter.


Вывод теста:
0 
1


Мы бы хотели перейти от тестов, которые требуют ручной обработки результатов к тестам, которые могут выполняться автоматически.
В Примере 1.3 мы добавим в тест сравнение ожидаемого результата и фактического значения, выведем ok если ожидаемый и фактический результаты совпали.
Если вывод будет not ok, значит где-то произошла ошибка.

Пример 1.3: Тестирование операторов массива, сравнение ожидаемого результата и фактического значения
<?php<br>$fixture = array();<br>
print count($fixture) == 0 ? "ok\n" : "not ok\n";<br> <br>
$fixture[] = 'element';<br>print count($fixture) == 1 ?
"ok\n" : "not ok\n";<br>?>
<br><br>
* This source code was highlighted with Source Code Highlighter.


Вывод теста:
ok
ok


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

Пример 1.4: Использование функции утверждения для тестирования операторов массива
<?php<br>$fixture = array();<br>
assertTrue(count($fixture) == 0);<br> <br>$fixture[] = 'element';<br>
assertTrue(count($fixture) == 1);<br> <br>function assertTrue($condition)<br>
{<br>  if (!$condition) {<br>    
throw new Exception('Assertion failed.');<br>
  }<br>}<br>?>
<br><br>
* This source code was highlighted with Source Code Highlighter.



Тест полностью автоматизирован. Вместо просто тестирования, которое мы выполняли в первой версии теста, мы получили автоматический тест.

Цель использования автоматических тестов — допускать меньше ошибок в коде.
Даже если Ваш код все еще не идеален, и Вы используете отличные тесты, начните практиковать автоматические тесты и Вы обнаружите значительное сокращение количества ошибок.
Автоматические тесты обеспечат Вам уверенность в своем коде. Опираясь на эту уверенность, Вы сможете вносить более смелые и решительные изменения в свою программу (Рефакторинг), сможете улучшить взаимоотношения с коллегами и клиентами.
Каждый день, уходя домой, Вы будете знать, что программа стала значительно лучше, чем она была утром.


Продолжение:
Часть 2, Часть 3, Часть 4
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 31
  • +3
    По таким простеньким примерам были статьи.
    А вот примеров того, как использовать юнит-тесты для функций, которые работают со сложными объектами или с базой данных, например, не видел. А хотелось бы :)
    • +7
      Я планирую серию статей по PHPUnit, сложность будет нарастать, постепенно доберусь и до более серьезных примеров.
      • +6
        Не хочу сглазить, но 95% циклов статей заканчивается основами и остаются лишь в головах авторов :-(
        ps: сам практикую тдд уже более 4 лет
        • 0
          Было бы очень неплохо.
          Мне таких статей сильно не хватает.
          Кстати, а использование PHPUnit фактически привяжет к нему свежеразработанный класс и вместе с ним будет должно переносится из проекта в проект?
          И как тестировать более сложные случаи? Например когда тестируеться класс работающий с неким внешним сервисом, который возвращает в ответ на запросы некие, не дай бог, «человекопонятные» и сложные для анализа данные или ответ сильно варьируется в зависимости от аргументов запроса?
          Кроме того, использование tdd означает, что надо создать интерфейс ипродумать структуру класса до начала работы и ее изменение в последствии приведет также к тому, что приедтся переписывать и тесты, да?
          • +1
            PHPUnit ничего не привязывает (даже наоборот отвязывает). Это просто тест этого самого класса, отдельно от него в другой папочке. Мы делаем тест, подключаем тестируем класс, создаем экземпляр класса, создаем «окружение» и тестируем методы. Отвязывает он потому, что используя его будет уверенность, что класс нормально заработает и в других условиях
            • +1
              «Кроме того, использование tdd означает, что надо создать интерфейс ипродумать структуру класса до начала работы и ее изменение в последствии приведет также к тому, что приедтся переписывать и тесты, да?»
              нет. как раз наоборот — тесты позволяют гораздо меньше думать об интерфейсах во время непосредственно реализации.
              тесты пишутся по мере надобности. появилась надобность в фиче А (а потому как она появилась — ты уже задумался об интерфейсе так или иначе). Напиши тест. Внедри _УЖЕ РАБОТАЮЩИЙ_ код в рабочее окружение. появится надобность в фиче Б — будешь думать. никакого планирования заранее.
              • 0
                Ну что сказать… как только немного разберусь с текучкой — продолжу читать документацию на эту тему и пробовать. TDD звучит все более интересно, хотя пока и не слишком понятно.
                Хабростатейки на эту тему тоже будут весьма полезны.
            • 0
              И, да, в статье не слова о PHPUnit.
            • +3
              Взаимодействие с базой в тестах habrahabr.ru/blogs/webdev/72716/ (статья моя).
            • +2
              Извините, а где тут phpUnit? Здесь я вижу применение exception, для обрабоки ошибок, это НЕ тестирование, это отлов ошибок. Могли бы уже try / catch описать, и как применять разные классы exception, в зависимости от класса, метод которого сгенерировал исключение.
              • +1
                Это перевод первой главы официальной документации по PHPUnit, глава является введением в использование этого framework.
                Думаю, автор не использует try / catch, чтобы не вносить ненужное усложнение в пример.

                А чем отлов ошибок отличается от тестирования? :)
                • 0
                  Извините, не увидел значка «перевод» сразу. Думаю, что введение можно было пропустить, это слишком очевидные решения, print или var_dump используют все начинающие программисты, и с удовольствием от них бы отказались в пользу чего-то более удобного, xdebug или phpUnit к примеру. Про отличия тестирования и отлова ошибок на мой взгляд написал ниже.
                  • 0
                    «Думаю, что введение можно было пропустить»
                    позволю не согласиться. очень многие программисты останавливаются на этапе мотивации. из разряда «зачем писать дополнительный код». как раз тут и получаем эволюцию тестов от ручных к автоматическим (наколеночным, но какая разница) и понимаем их профит.
                • 0
                  Дык, вроде ж не для отлова ошибок, а именно для тестирования. Потому что вся функциональная часть кода в блоке 1.1 и к try-catch не имеет отношения.
                  Правда этот метод тестирования из тех, что будет выполняться и на продакшне тоже, чего быть не должно, но в принципе тут на пальцах показано, как самому себе сделать автоматическое тестирование.
                  • +1
                    Прочитайте Топик, посвященный phpUnit в netBeans.

                    Exception можно и нужно генерировать в случае, когда не удалось подключение к БД (например из-за неверных параметров подключения), при отсутствии ответа от сервера. при открытии соединение через fsockopen и т.д. Ошибок в работе, а не в коде.

                    Тестирование же применять для отлова нестандартных вариантов работы приложения или семантических ошибок кода. Таких ошибок не должно быть в production, кроме того их обработка через exception приведет к усложнению кода (комменты и phpUnit тесты вместе с ними можно свернуть в IDE, чтобы не мешали чтению кода) и т.д.
                • –3
                  О, а давайте еще поговорим о том, что такое программирование?
                  • +1
                    Немного успел поработать, как с PHPUnit, так и SimpleTest. В первом (в отличие от второго) меня крайне расстроило отсутсвие втроенной поддержки генерации HTML отчтётов. Конечно, можно использовать сторонние доработки или фокусы типа

                    ob_start();
                    PHPUnit_TextUI_TestRunner::run(self::suite(), array(
                        'xmlLogfile' => $reportPath
                    ));
                    ob_end_clean();
                    
                    $report = Report::generate($reportPath);
                    


                    но это на мой взгляд не очень удобно.
                    • 0
                      Что мне больше всего нравится в PHPUnit, так это его отчеты о покрытии тестами. В Simpletest есть что-либо подобное?
                      • 0
                        Не совсем понял, о чём вы. В roadmap'е на версию 1.9 у них числится «code coverage», если речь об этом.
                        • 0
                          Да, я именно о нем. У всех свои недостатки — code coverage в simpletest доступен только через сторонний spike phpcoverage (насколько я понял из их сайта сейчас, не обновлявшийся аж с 2008 года), а в phpunit как вы заметили, нет автогенерации html-отчетов.
                          Правда, лично мне html-отчеты не нужны, запускаю тесты в основном из консоли и IDE.
                    • +2
                      Заметил интересную тенденцию на Хабре, когда мне нужно поресерчить на определенную тему, статья на эту тему тут же появляется на главной. НЛО и особенно автору спасибо!)
                      • 0
                        Да-да, тоже очень часто замечал такую «контекстную рекламу» :) Это не только с Хабром так.
                      • +1
                        Про основы есть неплохая статья уже почти десятилетней давности, но всё равно вполне актуальная: phpclub.ru/detail/article/phpunit

                        А ещё есть проект по коллективному переводу мануала: translated.by/you/phpunit-manual-3-3/
                        • 0
                          А ещё есть проект по коллективному переводу мануала: translated.by/you/phpunit-manual-3-3/
                          Какой-то непонятный сайт, пытался найти перевод, лазил-лазил пр содержанию, так ничего и не нашел.
                        • 0
                          На практике использовать assertTrue как показано выше не всегда удобно.
                          Лучше делать так (массив оставил только для соответствия примеру):

                          $result = $true;
                          $fixture = array();
                          $result = $result && (some condition);
                          $fixture[] = 'element';
                          $result = $result && (some condition);
                          assertTrue($result);

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

                          Если билд собирается раз в сутки, то и баги будут фикситься все дальше и дальше от времени их создания. Получим ту же проблему, от которой хотели избавиться при введении тестирования. Баг будет обнаружен поздно и «цена исправления станет высокой»! Поэтому ассерты в таких случаях не очень удобно использовать.

                          Это пример из реальной жизни, где тестят не массивы, а работу с базами данных и прочее, прочее.
                          • 0
                            Имхо это плохая рекомендация.

                            1. Очень странно, починив один тест не прогнать сразу же все тесты еще раз, и ждать билда раз в сутки (Кстати, у вас правда такая большая система, что собирается 5-6 часов? Мб стоит гонять тесты чаще?)

                            2. Тесты должны быть понятными. Их должно быть просто читать. Должно быть понятно где сломалось. Если в тесте делается пара десятков проверок и делается их конъюнкция то с ходу понять какое условие не прошло не ясно. Не говоря уже о том, что код теста усложняется и создаются условия для появления ошибок в самих тестах.
                            • 0
                              «Имхо это плохая рекомендация». Думаю, что именно для юнит тестов да, плохая! Если программист запускает тесты после написания кода, то да — Вы правы. Но в моем случае — это лучшая рекомендация, т.к. у нас тесты пускаются тест тимом после сборки.

                              1. Нет. Система очень большая. Собирается билд от получаса (для некоторых осей еще больше). Без полной сборки всех компонент использовать отдельные компоненты просто нереально, хотя было очень большое желание и попытки (зависимостей очень много, на юниксах особенно). Ставится билд тоже немало времени. На Федоре по 3 часа иногда уходит.

                              2. Тесты должны быть понятными. — 100%, так и есть
                              2.1. Их должно быть просто читать. — 100%, так и есть
                              2.2. Должно быть понятно где сломалось. — 100% и это реальная проблема, т.к. не все тесты удовлетворяют этому условию.
                              2.3. Если в тесте делается пара десятков проверок и делается их конъюнкция то с ходу понять какое условие не прошло не ясно. — да, но для этого есть п. 2.2. Сходу можно понять, если делать хороший лог и стек трейс теста выводить с шагами.
                              2.4. Не говоря уже о том, что код теста усложняется и создаются условия для появления ошибок в самих тестах. — да, у нас даже баги на тесты пишут. Даже в приведенном в этой статье случае, можно сделать баги, так что этот критерий не показателен. Продукт ооочень большой. А еще у нас есть коде ревью внутри тест тима, без которого пп. 2.1. и 2.2. были бы трудно достижимыми.

                              Я думаю от задач надо отталкиваться. Я описал свой опыт :) Кому-то он конечно не подойдет.
                            • 0
                              Как заметили ниже в тесте тоже можно сделать баг :) И я его сделал! Верный вариант такой:

                              $result = $true;
                              $fixture = array();
                              $result = (some condition) && $result;
                              $fixture[] = 'element';
                              $result = (some condition) && $result;
                              assertTrue($result);
                            • 0
                              Под автоматическими тестами обычно подразумеваются функциональные тесты, а xUnit используется для написания модульных тестов. Модульные тесты, в свою очередь, являются основополагающей частью разработки на основе тестирования (TDD) и их основной задачей не является тестирование кода. Более того, они обычно пишутся еще до того, как появляется какой-либо код.
                              • +2
                                Кстати, вот здесь очень много полезной информации, в частности по модульному и функциональному тестированию, причем на русском языке.
                                • 0
                                  то что вы тут написали можно элементарно сделать с помошью ассерт-ов, прям по ходу разработки, малейшее изменение и сразу видно где вылез бок, при заливке на лив, в худшем случае отключить асерты, в лучшем вырезать

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