Юнит тесты php. Основы Unit тестирования в PHP с помощью PHPUnit

Вы, ребята, проводите модульное тестирование на PHP? Я не уверен, что я когда-либо делал это... что это такое?

assertEquals(0, count($stack)); array_push($stack, "foo"); $this->assertEquals("foo", $stack); $this->assertEquals(1, count($stack)); $this->assertEquals("foo", array_pop($stack)); $this->assertEquals(0, count($stack)); } } ?>

В качестве более сложного примера я хотел бы указать вам на фрагмент my на github .

PHPUnit с охватом кода

Мне нравится практиковать что-то под названием TDD с использованием модульной системы тестирования (в PHP, которая phpunit).

Что мне также очень нравится в phpunit , так это то, что он также предлагает покрытие кода через xdebug. Как видно из изображения ниже, мой класс имеет 100% -ный охват тестирования. Это означает, что была проверена каждая строка из моего класса Authentication , что дает мне уверенность в том, что код делает то, что должен. Имейте в виду, что покрытие не всегда означает, что ваш код хорошо протестирован. Вы можете иметь 100% -ый охват без тестирования отдельной строки производственного кода.

Netbeans

Лично мне нравится тестировать свой код внутри Netbeans (для PHP). с простым щелчком мыши (alt + f6) я могу проверить весь свой код. Это означает, что мне не нужно оставлять IDE, что мне очень нравится, и помогает экономить время переключения между сеансами.

Этот учебный скрипт на PHP показывает, как можно описать некий тест в виде единственного массива вопросов и ответов. Поскольку типов вопросов может быть несколько (выбор "да"-"нет", выбор одного из нескольких вариантов, ввод числа или строки в качестве ответа), нам понадобится не просто массив, а массив массивов , каждый элемент которого будет описывать всё, что нужно для вывода и проверки очередного вопроса. Это будут записи со следующими ключами:

  • "q" - отображаемый текст вопроса;
  • "t" - тип вопроса, соответствующий нужному тегу HTML: "checkbox" для галочек "да/нет", "text" для строки или числа в качестве ответа, "select" - для списка, в котором нужно выбрать одно значение из нескольких. Выбор более одного значения реализуем придуманным нами элементом "multiselect" , представляющим из собой группу вместе обрабатываемых checkbox"ов. На самом деле, стандартный список "; break; case "text": $len = strlen ($val["a"]); echo $val["q"]." "; break; case "select": echo $val["q"]." "; break; case "multiselect": $i = explode ("|",$val["i"]); echo $val["q"].": "; foreach ($i as $number=>$item) echo $item." "; break; } echo "
    "; } echo ""; } function error_check ($q) { $question_types = array ("checkbox", "text", "select", "multiselect"); $error = ""; if (!isset($q["q"]) or empty($q["q"])) $error="Нет текста вопроса или он пуст"; else if (!isset($q["t"]) or empty($q["t"])) $error="Не указан или пуст тип вопроса"; else if (!in_array($q["t"],$question_types)) $error="Указан неверный тип вопроса"; else if (!isset($q["a"]) or empty($q["a"]) and $q["a"]!="0") $error="Нет текста ответа или он пуст"; else { if ($q["t"]=="checkbox" and !($q["a"]=="0" or $q["a"]=="1")) $error = "Для переключателя разрешены ответы 0 или 1"; else if ($q["t"]=="select" || $q["t"]=="multiselect") { if (!isset($q["i"]) or empty($q["i"])) $error="Не указаны элементы списка"; else { $i = explode ("|",$q["i"]); if (count($i)<2) $error="Нет хотя бы 2 элементов списка вариантов ответа с разделителем |"; foreach ($i as $s) if (strlen($s)<1) { $error = "Вариант ответа короче 1 символа"; break; } else { if ($q["t"]=="select" and !array_key_exists($q["a"],$i)) $error="Ответ не является номером элемента списка"; if ($q["t"]=="multiselect") { $a = explode ("|",$q["a"]); if (count($i)!=count($a)) $error="Число утверждений и ответов не совпадает"; foreach ($a as $s) if ($s!="0" and $s!="1") { $error = "Утверждение не отмечено как верное или неверное"; break; } } } } } } if (!empty($error)) { echo "

    Найдена ошибка теста: ".$error."

    Отладочная информация:

    "; print_r ($q); exit; } } function strlwr_($s){ $hi = "ABCDEFGHIJKLMNOPQRSTUVWXYZАБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ"; $lo = "abcdefghijklmnopqrstuvwxyzабвгдеёжзийклмнопрстуфхцчшщъыьэюя"; $len = strlen ($s); $d=""; for ($i=0; $i<$len; $i++) { $c = substr($s,$i,1); $n = strpos($c,$hi); if ($n!==FALSE) $c = substr ($lo,$n,1); $d .= $c; } return $d; } ?>

    Всем доброго времени суток! Сегодня я хотел бы поговорить с Вами о том, что такое модульное тестирование в PHP .

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

    В общем, конечно, данный подход неплох, однако у него есть существенные недостатки. Так, например, при написании, какого-либо достаточно крупного проекта, код будет постепенно засоряться закомментированными отладочными функциями, типа print или print_r .В случае работы над собственным проектом, код которого никто, ну или почти никто, читать не собирается, он, до некоторой поры, будет оправдан.

    Однако давайте представим такую ситуацию: Вы пишете сайты для заказчиков на собственной системе управления контентом. Заказчики довольны, Вы чувствуете себя прекрасно, но в один день понимаете, что система, которую Вы разработали, уже не отвечает предъявляемым к ней требованиям, она нуждается в изменениях. И Вы начинаете переписывать одну часть системы за другой.

    Наступает такой момент, когда один новый класс, метод, условие или цикл - рушат всю систему. Изменения в одном месте приводят к ошибкам в другом. И вот они уже лезут без конца, как из рога изобилия, и становиться понятно, что так дальше невозможно. А все было бы гораздо лучше, если бы сначала, помимо всего прочего, были бы написаны PHP Unit тесты . Не зря ведь говорит, Мартин Фаулер , что когда бы Вы ни пытались напечатать что-то через print в целях отладки или рефакторинга, лучше напишите это в виде Unit теста .

    Итак, с теорией вроде ознакомились, теперь перейдем непосредственно к коду. Здесь необходимо сделать важные замечания, все операции проводятся на ПК под управлением Windows 7 c установленным PHP 7 версии. Дальше будет по пунктам.

    2) Загруженный файл перемещаем в папкуC:\bin . В этой же папке создаем файл phpunit.bat , записываем в него следующее содержимое: @php C:\bin\phpunit-6.3.0.phar %*

    Учтите, что путь C:\bin должен быть определен в системной переменной PATH , иначе при попытке выполнить в консоли команду phpunit Вы получите ошибку!

    3) Откройте консоль и выполните команду phpunit , и если все правильно, то в консоли должна отобразиться справка.

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

    // файл StackTest.php, расположен в каталоге C:/Projects/php/tests
    // подключаем главный класс TestCase из пространства имен PHPUnit\Framework
    use PHPUnit\Framework\TestCase;

    // определяем тестируемый класс как наследник класса TestCase
    class StackTest extends TestCase
    {
    // тестируемые функции являются публичными, начинаются со слова test
    public function testPushAndPop()
    {
    $stack = ; // создали массив
    // и проверили утверждение assert на то, что число элементов в массиве равно нулю
    $this->

    $this->assertEquals("foo", array_pop($stack));
    $this->assertEquals(0, count($stack));
    }
    }
    ?>

    Код хорошо комментирован, однако поясню пару моментов. Краеугольным камнем Unit тестирования является утверждение (assertion ). Утверждение - это Ваше предположение об ожидаемом значении, или другими словами Вы утверждаете, что значение переменной, элемента массива, результат выполнения метода и т.д. будет равно такому-то значению. В примере выше, при первоначальном создании массива, ожидаемое значение его длины - 0. Так оно и есть на самом деле в нашем примере.

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

    Так тест мы написали, а что дальше? А дальше его надо запустить. Для этого открываем консоль, переходим в папку с нашим тестом (PHP Unit тесты обычно располагаются в отдельной папке tests ) и запускаем команду phpunit , передав ей в аргументе текущий каталог (обозначается одной точкой).

    cd C:/Projects/php/tests && phpunit .

    Данная команда автоматически пройдется по всем PHP тестам , которые есть в данном каталоге. По завершении выполнения, она выведет информацию о том, сколько тестов пройдено и, возможно, провалено.

    Таким образом, сегодня мы с Вами выяснили что такое Unit тестирование в PHP , что применять его не только полезно, но и нужно. А если Вы знаете PHP плохо, или не знаете его совсем, то специально для Вас у меня есть отличный видеокурс , в котором, я, в частности, подробно разбираю тему модульного (Unit) тестирования в PHP .

    версия для печати

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

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

    Поставщики данных (data providers)

    Возьмем пример сложнее (см. полную версию в в архиве с исходниками). Приведенный ниже метод возвращает фразу в правильном склонении в зависимости от числа, идущего с текстом:

    // src/Strings.php /** * Склонение слов в зависимости от числа * @param int $n число * @param array $s набор слов * @param bool $glued объединить результат с числом? Объединение будет через пробел * @return string */ public static function declination($n, array $s, $glued = true) { $n = $n % 100; $ln = $n % 10; $phrase = $s[(($n < 10 || $n > 20) && $ln >= 1 && $ln <= 4) ? (($ln == 1) ? 0: 1) : 2]; return $glued ? $n . " " . $phrase: $phrase; }

    Тестовый метод (см. ):

    // tests/StringsTest.php /** * Тест: склонение слов в зависимости от числа * * @dataProvider DeclinationProvider * @param int $num число * @param string $expect ожидаемый результат */ public function test_declination(int $num, string $expect) { $words = ["комментарий", "комментария", "комментариев"]; $result = Strings::declination($num, $words); $this->assertEquals($expect, $result, "Неверное склонение"); } /** * Данные: склонение слов в зависимости от числа * @return array */ public function DeclinationProvider() { return [ , , , ]; }

    Data providers - это фишка PHPUnit, возможно есть аналоги в других фреймворках. К любому тест-методу можно присоединить поставщика данных через тег @dataProvider . Требования в методу-поставщику: он должен возвращать двумерный массив значений. Каждый подмассив - это набор данных на один тест-случай. Подмассив должен содержать элементы в том же количестве и порядке, как как это требуется в параметрах тест-метода.

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

    Если в вашем поставщике данных возникнет исключение, тогда PhpUnit закончит тестирование с сообщением "No tests executed!" , что вообще ни о чем не говорит. Включайте отладчик и ищите, что у вас не так в DataProvider .

    Альтернатива использования поставщика данных - написать отдельные тест-методы на каждый случай. Или запилить все в один тест-метод, хотя я рекомендую избегать такого решения в больших тестах.

    Прим.: в том же тестовом классе есть пример, как бы выглядел тест без использования DataProvider .

    Еще один пример организации DataProvider см. в , метод test_checkAttrValueWithType() .

    Важно : во время выполнения теста сначала вызывается поставщик данных, потом setUpBeforeClass() и setUp() (о них в следующем разделе ). Т.е. в dataProvider() нельзя полагаться на данные, которые могут быть заданы в setUpBeforeClass() | setup() тестового метода.

    Фикстуры

    Одной и наиболее времязатратных частей написания тестов является установка "мира" приложения в известное состоянии и откат к этому состоянию после теста. Это известное состояние называется фикстурой теста. (перевод из мануала PHPUnit).

    PHPUnit позволяет организовывать тестовое окружение в отдельно взятом классе, а так же для каждого выполняемого теста в классе. Для этого есть группа методов, о которых подробно расписано в мануале PHPUnit: 4. Fixtures . Кратко расскажу.

    Приведенные ниже методы (если они вам нужны для тестов) нужно реализовывать прямо в ваших тестовых классах:

    • сначала вызов всех методов поставщиков данных
    • setUpBeforeClass() статичный метод, выполняется на этапе создания тест-класса. Аналогичный ему статический метод tearDownAfterClass() выполняется после всех тестов
    • Динамический метод setUp() выполняется перед каждым тестом, tearDown() после каждого теста
    • Динамический метод assertPreConditions() выполняется перед первым утверждением в каждом тесте. assertPostConditions() выполняется, если тест успешно завершился.

    Прим: найдите в мануале пример Example 4.2 и результат его выполнения. Этот пример показывает последовательность вызова всех методов фреймворка для установки фикстур.

    Как использовать эти методы - решать вам. Идея простая: все, что можно инициализировать для всех/каждого тест-метода - выносят в методы setUpBeforeClass() и setUp() соответственно. Если после теста требуется уборка (например, очистка кеша), вызываются соответствующие методы отката.

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

    Еще такой интересный момент: в unit-тестах можно принебречь оптимизацией или производительностью в пользу контролируемого поведения. Поэтому часто, вместо однократной инициализации окружения для всего класса через метод setUpBeforeClass() используют setUp() для создания известной среды для каждого тест-метода .

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

    Подмена зависимостей

    Подмена средствами PHPUnit

    Тут мне трудно было выбрать, что рассказывать в статье, а за чем отправить в мануал PHPUnit: 9. Test Doubles . Там всего одна страница, и на примерах изложено вполне доступно. Я не хочу переписывать эту часть из документациии. Но все же скажу пару слов.

    Как делается подмена зависимостей в PHPUnit 6.1. Напомню пример из теории (прим.: там класс назывался SomeClass ):

    // в исходниках: src/ClassDI.php class ClassDI { /** * подключение к БД * @var IDatabase $db */ private $db; /** * Внедрение зависимости в класс * @param IDatabase $db подключение к БД */ public function __construct(IDatabase $db) { $this->$db = $db; } /** * Какой-то боевой метод * * Внедрение еще одной зависимости, прямо в метод * * @param ILogger $logger объект логера * @return int */ public function doIt(ILogger $logger):int { // тут только чистый код, без инициализации зависимых систем $id = $this->db->query("INSERT ..."); $logger->add("Создана новая запись #" . $id); return $id; } }

    Тест с подменой зависимостей:

    // в исходниках: tests/dummy_examples/ClassDITest.php public function test_doIt() { // Создаем поддельный объект зависимого класса $dbStub = $this->createMock(IDatabase::class); // Описываем ожидаемое поведение поддельного метода $dbStub->method("query") ->willReturn(10); // Подделываем другую зависимость, сразу указываем, какой метод подменяем. $loggerStub = $this ->getMockBuilder(ILogger::class) ->setMethods(["add"]) ->getMock(); $id = (new ClassDI($dbStub))->doIt($loggerStub); $this->assertEquals(10, $id); }

    Собственно все.

    Как видно из кода выше, для подделки использовали разные методы. Дело в том, что метод createMock() - это обертка. На самом деле, в нем выполняется цепочка методов PHPUnit:

    $stub = $this ->getMockBuilder($originalClassName) ->disableOriginalConstructor() ->disableOriginalClone() ->disableArgumentCloning() ->disallowMockingUnknownTypes() ->getMock();

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

    Подделки могут возвращать не только скалярные значения. У PHPUnit есть методы на все возможные случаи подмены.

    Еще один пример подделки зависимости , метод test_validateAttributes() , блок кода Act .

    В тест-методах можно строить утверждения относительно подмененного объекта (понятие mock помните?). И вот тут еще большее поле для деятельности. У меня нет простого, но сколь-нибудь полезного примера, поэтому лучше смотрите примеры в мануале PHPUnit. Mock Objects , если у вас возникнет такая необходимость. Как было отмечено в теоретической части статьи, проверка взаимодействий - это самый крайний случай в модульном тестировании, когда иначе никак проверить метод не получается.

    PHPUnit позволяет проверить, что подменяемый метод вызван ожидаемое количество раз, с определенными аргументами или их последовательностью (если вызовов несколько). Так же можно описать условия, при которых ожидаются определенные аргументы. Я сильно в эту тему не вдавался, обычно хватает тестирования в первых двух направлениях (результат или состояние).

    Подмена с использованием Mockery

    Документация

    Mockery это PHP-фреймворк, запиленный специально для подмены объектов в unit-тестах. Он разработан, как альтернатива библиотеке подмены в PHPUnit , может использоваться в нем или как отдельный модуль, т.е. его можно подключить в другие тестовые движки. Основная фича - использование человекопонятного предметно-ориентированного языка (Domain-specific language, DSL).

    Простым примером предметно-ориентированного языка является SQL для СУБД.

    AspectMock проксирует все вызовы всех методов и позволяет их налету подменить. Я попробовал - это действительно круто. Но в итоге с моим движком он работать не смог, конфликтнул где-то. Т.о. возьмите на вооружение, если сможете подружить его с вашим проектом.

    Установка

    composer require --dev codeception/aspect-mock

    Настройка. В bootstrap.php тестов прописываем типа этого (в исходниках см. ):

    $kernel = \AspectMock\Kernel::getInstance(); $kernel->init([ "debug" => true, "includePaths" => [__DIR__ . "/../src"], "excludePaths" => [__DIR__ . "/../tests/"], "cacheDir" => __DIR__ . "/../temp/aspectMock/", ]);

    В PHPUnit метод expectException() , а так же директива @expectedException используются в тестах для указания ядру фреймворка "ожидать такое-то исключение" . В итоге тест считается пройденным если исключение возникло.

    И тут есть ньюансик: после того, как PHPUnit поймает ожидаемое исключение, выполнение тест-метода прекратится! Т.е. expectException() - это аналог assert-метода, только с прерыванием. Есть так же методы на проверку кода и сообщения исключения.

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

    1. забить на проверку исключений в принципе;
    2. писать тест-методы на нормальное поведение и на каждое пробрасываемое исключение отдельно;
    3. в тест-методе оформлять блоки try...catch ;
    4. использовать data provider .

    Последний вариант мне представляется наиболее предпочтительным. Причем data provider позволяет описать вообще все тест-кейсы в одном методе.

    См. скрипт архива исходников . Пример ExceptionsTest::test_normalizePriority() демонстрирует решение с data provider . Второй метод, test_getTargetFileName() для демонстрации исключения в одном методе вместе с нормальными ситуациями.

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

    Тестирование запросов в базу данных

    Тестирование взаимодействия с БД - это скорее интеграционный тест, но все же стоит один раз напрячься и сделать. Это несложно. Хотя зависит от проекта, я не настаиваю:)

    В мануле много всего расписано по этому вопросу, но я особо не занимался тестированием именно взаимодействия с БД. На практике написал несколько простых тестов с маленькой базой.

    В общих чертах: вам нужна будет еще одна база данных помимо боевой, структура должна соответствовать рабочей БД. Если используете миграции, поддержка актуальной структуры - не проблема. В PHPUnit нужно добавить модуль:

    composer require --dev phpunit/dbunit

    В тестовый класс нужно подключить трейт TestCaseTrait (до версии PHPUnit 6.1 был суперкласс для наследования, теперь трейт) и реализовать два абстрактных метода: подключение к базе и загрузка данных (фикстур) в таблицы.

    // tests/dummy_examples/DBEmptyTest.php use PHPUnit\Framework\TestCase; use PHPUnit\DbUnit\TestCaseTrait; use PHPUnit\DbUnit\DataSet\YamlDataSet; class MyGuestbookTest extends TestCase { use TestCaseTrait; /** * Соединение с тестовой базой * @return \PHPUnit\DbUnit\Database\DefaultConnection */ public function getConnection() { $pdo = new PDO("sqlite::memory:"); return $this->createDefaultDBConnection($pdo, ":memory:"); } /** * Загрузка данных в таблицы * @return YamlDataSet */ public function getDataSet() { return new YamlDataSet(__DIR__ . "/fixtures/dataset1.yml"); } }

    Прим: реализация подключения к базе зависит от конкретного приложения. Методы getConnection() и getDataSet() выполняются перед каждым тест-методом.

    Как это работает: PHPUnit подключается к базе, очищает таблицы и грузит в них данные, которые вы укажете. Ваш проверяемый класс должен уметь подключаться к тестовой БД. Далее обычная процедура тестирования - Arrange Act Assert . В конце - откат через , если требуется.

    PHPUnit предоставляет кучу форматов для загрузки данных: несколько XML форматов, YAML, CSV, arrays и какие-то велосипеды. Имхо, удобнее всего YAML.

    См. в архиве исходников пример подготовленных данных - , тест в

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


    Остальные части:


    Понравилась статья? Расскажите о ней друзьям.

    Знакомая ситуация: вы разрабатываете приложение, решаете проблемы и, иногда, возникает ощущение, что вы ходите по кругу. Правите один баг и сразу появляется другой. Иногда это тот, который вы поправили 30 минут назад, иногда — новый. Отладка становится очень сложной, но есть хороший и простой выход из этой ситуации. Юнит тесты могут не только уменьшить боль при разработке, но и помогут писать код, который легче сопровождать и легче изменять.

    Для понимания того, что такое модульное тестирование, необходимо определить понятие «модуля». Модуль (или unit ) — это часть функционала приложения результат работы которой мы можем проверить (или протестировать). Модульное тестирование — это, собственно проверка, что данный модуль работает именно так как ожидается.Написав один раз тесты, всякий раз когда вы внесете изменения в код, вам останется только запустить тесты, для проверки, что всё правильно. Таким образом вы всегда будете уверены, что своими изменениями вы не сломаете систему.

    Мифы о юнит тестировании

    Не смотря на всю пользу юнит тестирования, не все разработчики им пользуются. Почему? Есть несколько ответов на этот вопрос, но все они — не слишком хорошие оправдания. Рассмотрим распространенные причины и попытаемся разобраться почему они не оправданы.

    Написание тестов занимает слишком много времени

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

    Не надо тестов — код и так работает!

    Еще одно оправдание разработчиков: приложение работает — нет необходимости в тестировании. Они знают приложение и знают его слабые места, и смогут поправить все, что надо, иногда за несколько секунд. Но представьте, что для разработки приложения привлеки нового разработчика, который понятия не имеет как устроен код. Новичок может сделать какие-либо изменения, которые могут сломать что угодно. Юнит тесты помогут избежать подобных ситуаций.

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

    Пример

    Приступим к практике. В наших примерах мы будем использовать библиотеку PHPUnit. Самый простой способ установить PHPUnit — затянуть с PEAR канала.

    Pear config-set auto_discover 1 pear install pear.phpunit.de/PHPUnit

    Если все пройдет хорошо, то все необходимые инструменты будут установлены. Если вы хотите установить PHPUnit вручную — инструкцию вы найдете .

    Первый тест

    Используя PHPUnit, вы будете писать тестовые классы, содержащие тестовые методы и всё это должно удовлетворять следующим соглашениям:

    • В большинстве случаев вы будете наследовать класс PHPUnit_Framework_TestCase , что предоставит вам доступ к встроенным методам, например, setUp() и tearDown() .
    • Имя тестирующего класса образуется добавлением слова Test к имени тестируемого класса. Например, вы тестируете класс RemoteConnect , значит имя тестирующего — RemoteConnectTest .
    • Имена тестирующих методов всегда должны начинаться с “test” (например, testDoesLikeWaffles()). Методы должны быть публичными. Вы можете использовать приватные методы в своих тестах, но они не будут запускаться как тесты через PHPUnit.
    • Тестирующие методы не принимают параметров. Вы должны писать тестирующие методы максимально независимыми и самодостаточными. иногда это неудобно, но вы получите более чистые и эффективные тесты.

    Напишем небольшой класс для тестирования RemoteConnect.php:

    Если мы хотим протестировать функционал для соединения с удаленным сервером, то мы должны написать подобный тест:

    assertTrue($connObj->connectToServer($serverName) !== false); } } ?>

    Тестирующий класс наследует базовый PHPUnit класс, а значит и всю необходимую функциональность. Первые два метода — setUp и tearDown — пример этой встроенной функциональности. Это вспомогательные функции, которые являются частью каждого теста. Они выполняются до запуска всех тестов и после соответственно. Но сейчас нас интересует метод testConnectionIsValid . Этот метод создает объект типа RemoteConnect , и вызывает метод connectToServer .

    Мы вызываем еще одну вспомогательную функцию assertTrue в нашем тесте. Эта функция определяет простейшее утверждение (assertion): она проверяет является ли переданное значение истиной. Другие вспомогательные функции выполняют проверки свойств объектов, существования файлов, наличия ключей в массиве, или соответствия регулярному выражению. В нашем случае мы хотим убедиться в правильности подключения к удаленному серверу, т.е. в том, что функция connectToServer возвращает true .

    Запуск тестов

    Запускаются тесты простым вызовом команды phpunit с указанием вашего php файла с тестами:

    Phpunit /path/to/tests/RemoteConnectTest.php

    PHPUnit запускает все тесты и собирает немного статистики: успешно завершился тест или нет, количество тестов и утверждений (assertions) и выводит все это. Пример вывода:

    PHPUnit 3.4 by Sebastian Bergmann . Time: 1 second Tests: 1, Assertions: 1, Failures 0

    Для каждого выполненного теста будет выведен результат: «.» если тест завершился успешно, “F” если тест не пройден, “I” если тест невозможно завершить и “S” если тест был пропущен.

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

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

    AssertTrue / AssertFalse Проверка переданных значений на равенство true/false
    AssertEquals Проверка переданных значений на равенство
    AssertGreaterThan Сравнивает две переменные (есть так же LessThan, GreaterThanOrEqual, and LessThanOrEqual)
    AssertContains Содержит ли переданная переменная заданное значение
    AssertType Проверка типа переменной
    AssertNull Проверка на равенство null
    AssertFileExists Проверка существования файла
    AssertRegExp Провка по регулярному выражению

    Например есть функция, которая возвращает объект (returnSampleObject) и мы хотим убедиться в том, что возвращаемый объект будет нужного нам типа:

    returnSampleObject(); $this->assertType("remoteConnect", $returnedObject); } ?>

    Один тест — одно утверждение (assert)

    Как и во всех областях разработки программного обеспечения, в тестировании есть лучшие практики. Одна из них — «один тест — одно утверждение» (one test, one assertion). Это правило поможет писать небольшие и легко читаемые тесты. Но иногда возникают мысли: «Раз уж мы здесь проверяем это, то и кое-что другое заодно проверим!». Например:

    assertGreaterThan(0,strlen($string)); $this->assertContains(“42”,$string); } ?>

    Наш тест testIsMyString проводит два разных теста. Сначала тест на пустую строку (длина должна быть > 0), затем тест на содержание в строке подстроки «42». Но этот тест может провалиться как в первом так и во втором случае, а сообщение об ошибке в обоих случаях будет одинаковым. Поэтому стоит придерживаться принципа «один тест — одно утверждение».

    Test-driven Development (разработка через тестирование)

    Было бы нехорошо, говоря о тестировании не упомянуть о распространенной технике разработки — разработке через тестирование (test driven development ). TDD — это техника, используемая при разработке программного обеспечения. Основная идея этой техники заключается в том, что сначала пишутся тесты и только после написания тестов пишется код приложения, который пройдет эти тесты.