В первых двух частях этой серии (« Введение в модульное тестирование в PHP с помощью PHPUnit» и « Ознакомление с утверждениями PHPUnit» ) я рассказал вам о некоторых шагах, которые можно предпринять для тестирования приложения. Я показал вам, как с помощью PHPUnit вы можете писать настолько простые или сложные тесты, сколько вам нужно для обеспечения качества.
К настоящему времени, я надеюсь, вы понимаете важность наличия хорошего набора юнит-тестов. Вы знаете, что, обладая надежной поддержкой, вы можете по желанию изменять большие части своего приложения и быть уверенным, что результат будет соответствовать 100%. В этой третьей части серии я собираюсь изучить две более продвинутые функции, которые могут не проявиться в вашей повседневной практике модульного тестирования.
PHPUnit имеет множество расширенных функций, которые могут быть удивительно полезны, когда возникает этот особый случай. Это включает в себя расширение самой платформы, создание тестовых наборов, создание статических наборов данных и фокус этой статьи: аннотации и макеты . Не волнуйтесь, если вы не уверены, что из этого; Я здесь, чтобы помочь. В конце я надеюсь, что вы увидите, как эти две функции могут быть полезны как для вас, так и для ваших тестов.
Аннотации
Если вы когда-либо читали книгу или делали заметки, вы будете знакомы с идеей аннотаций. В самом строгом смысле даже комментарии, которые вы помещаете в свои тесты, могут рассматриваться как аннотации. Например:
<?php class MyTestClass extends PHPUnit_Framework_TestCase { /** * Testing the answer to “do you love unit tests?” */ public function testDoYouLoveUnitTests() { $love = true; $this->assertTrue($love); } } ?>
В приведенном выше примере вы можете увидеть простой блок комментариев над определением функции. Если вы уже давно работаете с PHP, вы, несомненно, видели все виды стилей комментирования. Этот пример является «лучшей практикой», которую несколько популярных IDE будут использовать при вставке комментария.
В этом нет ничего волшебного, и обычный /* */
или //
подойдет, если вы действительно этого хотите. Трюк приходит, когда PHPUnit входит в картину. Использование стиля комментария из нашего примера позволяет PHPUnit автоматически выполнять некоторые интересные действия.
В PHPUnit есть набор аннотаций, которые вы можете использовать как в реальных тестах, так и в процессе генерации тестов — это верно, он может сделать некоторую тяжелую работу за вас.
В PHP отсутствует способ непосредственного разбора комментариев, поэтому в PHPUnit есть собственный инструмент, который ищет определенные фразы и символы для соответствия. Начнем с одной из более удобных функций — аннотаций, помогающих в создании тестов. Давайте начнем с примера класса, который выполняет некоторые основные математические операции:
<?php class MyMathClass { /** * Add two given values together and return sum */ public function addValues($a,$b) { return $a+$b; } } ?>
Легко, правда? Я придерживаюсь базового класса с одним методом, потому что хочу, чтобы вы наверняка уловили все происходящее.
С этим базовым классом мы хотим быть уверены, что законы физики и теории чисел никогда не изменятся. Итак, мы хотим написать тест. Конечно, мы можем написать некоторый код и быстро вывести тест из строя, но зачем это делать, если мы можем быть ленивыми и сделать так, чтобы PHPUnit сделал это за нас?
Благодаря Skeleton Generator мы можем указать его на класс, и он сделает все возможное, чтобы сделать нас тестом.
Поскольку это просто часть нормальной функциональности PHPUnit, мы просто вызываем phpunit
, но со специальным флагом. Давайте сохраним наш простой класс выше в файле локального каталога с именем MyMathClass.php
. Чтобы использовать генератор скелета, просто наведите его на файл класса:
./phpunit –skeleton-test MyMathClass
Тестовый файл будет показан с другой стороны — MyMathClassTest.php — содержащий:
<?php require_once '/path/to/MyMathClass.php'; /** * Test class for MyMathClass. * Generated by PHPUnit on 2011-02-07 at 12:22:07. */ class MyMathClassTest extends PHPUnit_Framework_TestCase { /** * @var MyMathClass */ protected $object; /** * Sets up the fixture, for example, opens a network connection. * This method is called before a test is executed. */ protected function setUp() { $this->object = new MyMathClass; } /** * Tears down the fixture, for example, closes a network connection. * This method is called after a test is executed. */ protected function tearDown() { } /** * @todo Implement testAddValues(). */ public function testAddValues() { // Remove the following lines when you implement this test. $this->markTestIncomplete( 'This test has not been implemented yet.' ); } } ?>
Как видите, он генерирует множество основных методов и свойств, которые могут вам понадобиться.
Тем не менее, у него есть некоторые ограничения. Генератор видит, что у вас есть собственный метод ( addValues
), но он не знает, что он делает. Конечно, наш тест очень прост, и он может быть в состоянии разобрать его и выяснить, но как насчет некоторых других тех сверхсложных методов, которые есть в вашем приложении.
Я знаю, что не хотел бы, чтобы автоматизированный инструмент пытался угадать функциональность и случайно удалял всю базу данных в процессе. Нет, PHPUnit умнее этого, выбирая легкий путь: он позволяет вам определить его и пометить как неполный.
Для большого класса это может сэкономить вам массу времени, печатая каждый из методов и общие сведения о тесте и классе. Однако внутри функциональности аннотаций есть скрытый драгоценный камень, который может сделать все это еще проще.
С помощью аннотации @assert
вы можете немного рассказать PHPUnit о том, что находится внутри ваших методов. Давайте еще раз посмотрим на наш метод addValues
:
<?php class MyMathClass { /** * Add two given values together and return sum * @assert (1,2) == 3 */ public function addValues($a,$b) { return $a+$b; } } ?>
Если мы phpunit –skeleton-test
запустим нашу phpunit –skeleton-test
, вы заметите разницу в файле, который она создает для теста. Взгляните на последний метод:
<?php /** * Generated from @assert (1,2) == 3. */ public function testAddValues() { $this->assertEquals( 3, $this->object->addValues(1,2) ); } ?>
Внезапно, PHPUnit имеет рентгеновское зрение и может заглянуть внутрь вашего метода! Ну вроде.
Очевидно, что он может генерировать такой тест, потому что мы сказали ему, что этот тест должен был делать. Вы можете использовать аннотацию @assert
с большинством логических видов вычислений: равно, больше, равно, меньше или равно и т. Д.
Вы даже можете сложить их, чтобы иметь несколько утверждений в тесте. Однако он ограничен значениями и константами — его нельзя использовать для магического создания объектов в сгенерированном тесте.
Еще одна удобная аннотация — @dataProvider
, простой способ @dataProvider
стандартный набор данных в свои тесты, чтобы вы всегда знали, с чем работаете.
Ясно как грязь? Позвольте привести пример.
Мы протестируем функциональность приложения с помощью методов, которые зависят только от ввода. То есть, нет внешних XML-файлов или баз данных, из которых он может извлечь данные примера. Вместо этого нам нужен способ определить набор данных, который могут использовать все тесты.
Один из вариантов — определить свойство test с некоторыми образцами данных и использовать его каждый раз, но аннотация @dataProvider
делает его еще проще.
Давайте придерживаться нашего простого метода addValues в нашем классе MyMathClass
. На этот раз, однако, мы собираемся представить новый элемент — простой набор данных, заменяющий наши жестко закодированные значения:
<?php /** * Data provider for test methods below */ public static function provider() { return array( array(1,2,3), array(4,2,6), array(1,5,7) ); } /** * Testing addValues returns sum of two values * @dataProvider provider */ public function testAddValues($a,$b,$sum) { $this->assertEquals( $sum, $this->object->addValues($a,$b) ); } ?>
Основным дополнением к нашему тесту является метод provider
. @dataProvider
это с аннотацией @dataProvider
, и получится немного веселья.
Для каждого из этих значений в массиве, возвращенном поставщиком, выполняется метод теста. Это позволяет легко проверять несколько наборов данных на одном тесте. Значения возвращаются в соответствующие параметры в методе testAddValues
: индекс массива от 0
до $a
, индекс массива от 1
до $b
и т. Д.
Первые два набора данных в моем возвращаемом массиве будут проходить очень хорошо, но, кажется, у меня иногда возникают проблемы с добавлением, и мой последний набор данных неверен: 1 + 5 не равно 7. Это показывает, что данные не всегда нужно сдавать экзамен.
Иногда имеет смысл иметь «плохие» данные в провайдере. Это может быть полезно при разработке через тестирование, когда вы знаете, на какие данные они должны правильно реагировать, но пока нет поддержки.
Это всего лишь две удобные аннотации, которые вы можете использовать при тестировании. Есть довольно много других (вы можете найти их в руководстве по PHPUnit ), которые делают действительно классные вещи:
- Аннотация
@expectedException
отслеживает возникновение исключения. Если это не так, тест не пройден. - Аннотация
@covers
может быть полезна для тех, кто беспокоится о числах покрытия кода и пытается убедиться, что представлен каждый бит кода, даже если один тест эффективно охватывает три различных метода. - Вы также можете использовать
@depends
для определения отношений между вашими тестами, если вам требуется, чтобы тест, на который опирается метод, прошел. Если этого не произойдет, следующий тест будет пропущен.
осмеяние
Отсюда мы переходим ко второй более продвинутой теме: насмешка .
Эта функция также называется «заглушкой» или «двойным тестом». Это немного сложнее, чем аннотации, поэтому потребуется немного больше объяснений.
Как и раньше, мы создадим простую ситуацию, чтобы понять, что может предложить насмешка. Сначала я объясню немного, что такое насмешка. Для тех, кто не знаком с этим термином, он приходит из практики «макетирования» тестовой или примерной версии предмета.
С помощью фиктивной функциональности PHPUnit вы создаете нечто, похожее на полный объект, но, когда вы выполняете определенное действие, оно дает только один результат.
Это будет иметь больше смысла, если мы просто посмотрим на пример класса и протестируем. В нашем примере мы будем работать с подключением к базе данных.
Как правило, база данных является счастливой и запросы возвращаются в течение секунды. Однако есть один запрос, который занимает столько ресурсов, что его не стоит запускать каждый раз. Он печально известен продолжительной работой скриптов и может задержать выполнение ваших модульных тестов (что может привести к задержке развертывания).
К счастью, насмешка может прийти на помощь и создать «поддельный объект» для этого метода. Вот класс:
<?php class Database { /** * This query always takes a really long time */ public function reallyLongTime() { // database query would return into $results $results = array( array(1,'test','foo value') ); sleep(100); return $results; } } ?>
На самом деле все просто — это базовый класс для определения метода для выполнения запроса, который может занять некоторое время.
Ради примера я сократил это до оператора return. Видишь этот sleep
я там бросил? Таким образом, мы можем легче сказать, что мы используем фиктивный объект вместо реального. Имитирует задержку в исполнении. Теперь самое интересное — тест.
Я объясню различные части после кода:
<?php require_once '/path/to/Database.php'; class DatabaseTest extends PHPUnit_Framework_TestCase { private $db = null; public function setUp() { $this->db = new Database(); } public function tearDown() { unset($this->db); } /** * Test that the "really long query" always returns values */ public function testReallyLongReturn() { $mock = $this->getMock('Database'); $result = array( array(1,'foo','bar test') ); $mock->expects($this->any()) ->method('reallyLongTime') ->will($this->returnValue($result)); $return = $mock->reallyLongTime(); $this->assertGreaterThan(0,count($return)); } } ?>
с<?php require_once '/path/to/Database.php'; class DatabaseTest extends PHPUnit_Framework_TestCase { private $db = null; public function setUp() { $this->db = new Database(); } public function tearDown() { unset($this->db); } /** * Test that the "really long query" always returns values */ public function testReallyLongReturn() { $mock = $this->getMock('Database'); $result = array( array(1,'foo','bar test') ); $mock->expects($this->any()) ->method('reallyLongTime') ->will($this->returnValue($result)); $return = $mock->reallyLongTime(); $this->assertGreaterThan(0,count($return)); } } ?>
Здесь делается несколько новых звонков; проще всего обнаружить метод getMock
в объект теста PHPUnit. Этот метод делает вызов для создания псевдо-версии рассматриваемого объекта. В этом случае создается фиктивный объект Database
который мы можем использовать в нашем тесте.
Технически этот объект можно использовать как любой другой. Вы можете вызвать другие методы и заставить их возвращаться так же, как обычно. Тем не менее, насмешливая функциональность облегчает это.
В нашем длинном примере запроса мы создаем фиктивный объект и переопределяем метод reallyLongTime
для наших собственных целей. Эти несколько строк являются реальным ключом:
<?php $mock->expects($this->any()) ->method('reallyLongTime') ->will($this->returnValue($result)); ?>
Эти три строки сообщают фиктивному объекту, что всякий раз, когда к нему reallyLongTime
метод reallyLongTime
, он должен вместо этого возвращать значение в $result
. Это обходит сверхдолгое время ожидания, и вы все еще можете проверить и убедиться, что содержимое возвращено правильно.
Затем, если когда-нибудь вы узнаете о магии индексов и кэширования и захотите переключиться на реальный объект, это совсем несложно. Затем вы можете вызвать метод reallyLongTime
для фиктивного объекта и получить предварительно определенный набор результатов.
С функциональностью насмешки можно получить массу удовольствия, включая использование удобного MockBuilder . Это позволяет вам делать некоторые звонки, которые не делает базовый макет. В нашем примере ниже вы можете увидеть, как создать новый макет объекта, который не использует конструктор класса, а также отключить автозагрузку, которая может быть в работе:
<?php public function testReallyLongRunBuilder() { $stub = $this->getMockBuilder('Database') ->setMethods(array( 'reallyLongTime' )) ->disableAutoload() ->disableOriginalConstructor() ->getMock(); $result = array(array(1,'foo','bar test')); $stub->expects($this->any()) ->method('reallyLongTime') ->will($this->returnValue($result)); $this->assertGreaterThan(0,count($return)); } ?>
Это работает аналогично нашим предыдущим примерам, за исключением двух новых методов: disableAutoload
и disableOriginalConstructor
. К счастью, они довольно описательны в том, что они делают.
Есть также несколько других типов возврата, кроме returnValue
. Вы можете получить значение, возвращаемое из функции обратного вызова (функция PHP или пользовательская), возвращать серию результатов при последовательных вызовах фиктивного объекта и даже выдавать исключение.
Метод with может использоваться для определения того, какой тип ввода может принимать макетируемый метод (и может также включать некоторую проверку типов). Например:
<?php /** * Testing enforcing the type to "array" like the "enforceTypes" * method does via type hinting */ public function ttestReallyLongRunBuilderConstraint() { $stub = $this->getMock('Database',array('reallyLongTime')); $stub->expects($this->any()) ->method('reallyLongTime') ->with($this->isType('array')); $arr = array('test'); $this->assertTrue($stub->reallyLongRun($arr)); } ?>
В этом случае тест будет reallyLongRun
потому что я reallyLongRun
с массивом (проверяется с помощью вызова isTyp
e). Если бы я вызвал его на заглушку со строкой, PHPUnit выбросил бы ошибку.
Если вам когда-нибудь понадобится смоделировать класс, работающий с файлами в локальной файловой системе, PHPUnit не сможет сделать это за вас; однако они рекомендуют потоковую оболочку vfsStream, которая может выступать в роли посредника и имитировать файловую систему.
Резюме
Я ознакомил вас с двумя из более мощных функций, которые PHPUnit может предложить, помимо обычных повседневных процедур тестирования.
Аннотации дают вам больший контроль над тем, как выполняются и генерируются ваши тесты, а макетирование дает вам мощный способ проверить, как должен работать код, без хлопот.
В руководстве по PHPUnit есть множество других интересных функций, ожидающих открытия. Вы можете сгенерировать документацию из своего теста с TestDox , интегрировать свое тестирование с Selenium и сгенерировать номера покрытия кода для своего приложения, чтобы указать вам, где именно вам нужно тестировать.
Оттуда вы можете перейти к таким понятиям, как проектирование на основе тестирования (упомянутое в предыдущей статье), которое может радикально изменить способ написания приложений.
Модульное тестирование — это удивительно мощный инструмент, а наличие программного обеспечения, такого как PHPUnit, делает его легким и приятным.