Статьи

Базовый TDD в вашем новом пакете PHP

В Diffbot части мы настроили нашу среду разработки, выполнив некоторые правила, унаследованные от The League , и создали два примера, но бесполезных классов — Diffbot и DiffbotException . В этой части мы начнем с Test Driven Development.

Если вы хотите следовать, пожалуйста, прочитайте Часть 1 или клонируйте часть 1 части этого руководства.

PHPUnit

Мы до некоторой степени рассмотрели PHPUnit ( 1 , 2 , 3 , 4 , 5 , 6 ), но пришло время применить его на практике. Сначала давайте проверим, установлен ли он.

 php vendor / phpunit / phpunit / phpunit 

Выполнение этой команды должно создать отчет, в котором говорится, что один тест пройден. Это тест, включенный в «Скелет лиги» по умолчанию, и он утверждает, что true , на самом деле, true . Отчет о покрытии также будет создан и помещен в папку сборки.

Если вы откроете этот отчет о покрытии в браузере, вы увидите, что у нас низкий показатель покрытия.

Теперь, когда мы уверены, что PHPUnit работает, давайте что-нибудь протестируем. В настоящее время у нас есть немного больше, чем геттеры и сеттеры в нашем классе, которые обычно не тестируются . Итак, что мы можем проверить в нашем текущем коде? Хорошо .. как насчет действительности предоставленного токена через создание экземпляра?

Сначала давайте рассмотрим файл конфигурации PHPUnit XML, phpunit.xml.dist . После изменения слова «Лига» на «Diffbot» это выглядит так:

 <?xml version="1.0" encoding="UTF-8"?> <phpunit bootstrap="vendor/autoload.php" backupGlobals="false" backupStaticAttributes="false" colors="true" verbose="true" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" stopOnFailure="false"> <testsuites> <testsuite name="Diffbot Test Suite"> <directory>tests</directory> </testsuite> </testsuites> <filter> <whitelist> <directory suffix=".php">src/</directory> </whitelist> </filter> <logging> <log type="tap" target="build/report.tap"/> <log type="junit" target="build/report.junit.xml"/> <log type="coverage-html" target="build/coverage" charset="UTF-8" yui="true" highlight="true"/> <log type="coverage-text" target="build/coverage.txt"/> <log type="coverage-clover" target="build/logs/clover.xml"/> </logging> </phpunit> во <?xml version="1.0" encoding="UTF-8"?> <phpunit bootstrap="vendor/autoload.php" backupGlobals="false" backupStaticAttributes="false" colors="true" verbose="true" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" stopOnFailure="false"> <testsuites> <testsuite name="Diffbot Test Suite"> <directory>tests</directory> </testsuite> </testsuites> <filter> <whitelist> <directory suffix=".php">src/</directory> </whitelist> </filter> <logging> <log type="tap" target="build/report.tap"/> <log type="junit" target="build/report.junit.xml"/> <log type="coverage-html" target="build/coverage" charset="UTF-8" yui="true" highlight="true"/> <log type="coverage-text" target="build/coverage.txt"/> <log type="coverage-clover" target="build/logs/clover.xml"/> </logging> </phpunit> во <?xml version="1.0" encoding="UTF-8"?> <phpunit bootstrap="vendor/autoload.php" backupGlobals="false" backupStaticAttributes="false" colors="true" verbose="true" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" stopOnFailure="false"> <testsuites> <testsuite name="Diffbot Test Suite"> <directory>tests</directory> </testsuite> </testsuites> <filter> <whitelist> <directory suffix=".php">src/</directory> </whitelist> </filter> <logging> <log type="tap" target="build/report.tap"/> <log type="junit" target="build/report.junit.xml"/> <log type="coverage-html" target="build/coverage" charset="UTF-8" yui="true" highlight="true"/> <log type="coverage-text" target="build/coverage.txt"/> <log type="coverage-clover" target="build/logs/clover.xml"/> </logging> </phpunit> 

Атрибуты основного элемента говорят PHPUnit сделать свой отчет как можно более подробным и преобразовывать все типы уведомлений и ошибок в исключения, а также некоторые другие типовые значения по умолчанию, с которыми вы можете ознакомиться на их веб-сайте. Затем мы определяем наборы тестов — наборы тестов, применяемые к данному приложению или сценарию. Одним из таких наборов является основной набор приложений (единственный, который мы будем использовать), и мы называем его «Diffbot Test Suite», определяя каталог tests в качестве хоста тестов — вы заметите, что пример теста Лиги уже внутри этого каталога. Мы также просим PHPunit игнорировать все файлы PHP в каталоге src/ (мы хотим, чтобы он запускал только тесты, а не наши классы), и, наконец, мы настраиваем ведение журнала — что он сообщает, как и где.

Давайте построим наш первый тест. В папке tests создайте DiffbotTest.php . Если вы используете PhpStorm, это почти автоматически:

Не забудьте проверить, что пространство имен в composer.json соответствует этому:

 "autoload-dev": { "psr-4": { "Swader\\Diffbot\\Test\\": "tests/" } }, 

Не стесняйтесь сейчас удалить ExampleTest (а также SkeletonClass) и заменить содержимое нашего класса DiffbotTest следующим:

 <?php namespace Swader\Diffbot\Test; use Swader\Diffbot\Diffbot; use Swader\Diffbot\Exceptions\DiffbotException; /** * @runTestsInSeparateProcesses */ class DiffbotTest extends \PHPUnit_Framework_TestCase { public function invalidTokens() { return [ 'empty' => [ '' ], 'a' => [ 'a' ], 'ab' => [ 'ab' ], 'abc' => [ 'abc' ], 'digit' => [ 1 ], 'double-digit' => [ 12 ], 'triple-digit' => [ 123 ], 'bool' => [ true ], 'array' => [ ['token'] ], ]; } public function validTokens() { return [ 'token' => [ 'token' ], 'short-hash' => [ '123456789' ], 'full-hash' => [ 'akrwejhtn983z420qrzc8397r4' ], ]; } /** * @dataProvider invalidTokens */ public function testSetTokenRaisesExceptionOnInvalidToken($token) { $this->setExpectedException('InvalidArgumentException'); Diffbot::setToken($token); } /** * @dataProvider validTokens */ public function testSetTokenSucceedsOnValidToken($token) { Diffbot::setToken($token); $bot = new Diffbot(); $this->assertInstanceOf('\Swader\Diffbot\Diffbot', $bot); } } 

В этом чрезвычайно простом примере мы тестируем статический метод Diffbot::setToken . Мы используем синтаксис DataProvider в PHPUnit для автоматической подачи значений в цикл (большое спасибо Мэтью Вейеру О’Пинни за исправление моего курса в этом). Это также позволяет нам узнать, какой из ключей не прошел тестирование, а не просто ожидать или не ожидать исключения. Если мы сейчас запустим тест и посмотрим на покрытие, мы должны увидеть что-то вроде этого:

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

 public function testInstantiationWithNoGlobalTokenAndNoArgumentRaisesAnException() { $this->setExpectedException('\Swader\Diffbot\Exceptions\DiffbotException'); new Diffbot(); } public function testInstantiationWithGlobalTokenAndNoArgumentSucceeds() { Diffbot::setToken('token'); $bot = new Diffbot(); $this->assertInstanceOf('Swader\Diffbot\Diffbot', $bot); } public function testInstantiationWithNoGlobalTokenButWithArgumentSucceeds() { $bot = new Diffbot('token'); $this->assertInstanceOf('Swader\Diffbot\Diffbot', $bot); } 

Эти методы охватывают все случаи конструктора — создание экземпляра без токена и без глобально установленного экземпляра, создание экземпляра с глобально установленным экземпляром и создание экземпляра только с параметром токена. Однако, запустив тест, мы увидим, что он провалился. Это происходит потому, что класс Diffbot по-прежнему имеет статическое свойство, установленное в предыдущем тесте, следовательно, отсутствует статический токен по умолчанию при использовании во втором тесте. Это распространенная проблема при тестировании глобальных переменных и статики . Чтобы обойти это, мы позаботимся о том, чтобы каждый наш тест в классе DiffbotTest выполнялся в отдельном процессе. Это будет выполняться намного медленнее, но обеспечит свежую и незагрязненную среду.

Добавьте следующую аннотацию над классом Declaraiton, например, так:

 /** * @runTestsInSeparateProcesses */ class DiffbotTest extends \PHPUnit_Framework_TestCase 

Теперь, если вы запустите тест и посмотрите на покрытие, вы заметите, что мы на 100% зеленые!

Это своего рода антишаблон и обычно указывает на то, что что-то не так с дизайном класса, если ему нужны отдельные процессы для тестирования, но я пока не нашел лучшего подхода для тестирования этого. Статическое свойство в классе Diffbot должно быть изменяемым для простоты использования — если у вас есть предложения по улучшению этого, я весь слух. Альтернативный подход к решению этой проблемы — создание метода reset или некоторых дополнительных установщиков, которые вы можете использовать, чтобы вручную вернуть класс в исходное состояние, но я избегаю этого подхода, чтобы не загрязнять мой класс связанной с тестами логикой. Словом, это тоже можно решить с помощью backupStaticAttributes , но я пока не смог заставить его работать.

TDD

В TDD вы, как правило, должны думать о функциональности, затем проверять ее (и терпеть неудачу), а затем реализовывать, чтобы она работала. Вот где тестирование движет вашей разработкой, следовательно, разработкой, управляемой тестированием . Это именно то, что мы будем делать в этом разделе.

Diffbot, как сервис, предлагает несколько API по умолчанию:

  • Article API извлекает структурированные данные из контента типа статьи, такого как новости и сообщения в блоге
  • API продукта извлекает информацию о продуктах. Отправьте его по ссылке на продукт, и вы получите информацию о цене, доступности, спецификациях и многом другом.
  • Image API получает информацию об изображении или наборе изображений, если вы передаете ему ссылку на страницу с несколькими
  • Analyze API автоматически определяет, какой из трех вышеуказанных API использовать, и автоматически применяет его. Он пытается использовать подход, который дает наибольшее количество информации при задании URL.
  • API для видео и обсуждения все еще находятся в разработке. Видео — это то же самое, что и API изображений, но для видеофайлов, в то время как Обсуждение может извлекать цепочки обсуждений из форумов, разделы комментариев на различных сайтах и ​​сообщения в социальных сетях и многое другое.

Как видно из документации, каждый из API возвращает аналогичный ответ (все возвращают действительный JSON), но возвращаемые поля в основном отличаются. Вот как я вижу класс Diffbot как конечный продукт — у него есть методы для каждого типа API, и каждый тип API — это отдельный класс, который нам еще предстоит разработать. Все эти классы API расширяют один абстрактный класс API, который содержит установщики для общих полей, но каждый класс API также содержит свои настраиваемые поля. Короче говоря, я хотел бы сделать возможными следующие подходы:

 $diffbot = new Diffbot('myToken'); $productAPI = $diffbot->createProductAPI('http://someurl.com'); $productAPI ->setTimeout(3000) ->setFields(['prefixCode', 'productOrigin']); $response = $productAPI->call(); // OR, LIKE THIS $response = $diffbot ->createProductAPI('http://someurl.com') ->setTimeout(0) ->setPrefixCode(true) ->setProductOrigin(true) ->setHeaderCookie(['key' => 'value', 'key2' => 'value2']) ->call(); 

Тестирование абстрактных классов

Для создания подклассов API нам понадобится общий абстрактный класс API, который будет расширяться. Но как мы можем тестировать абстрактные классы, не расширяя их? С двойным испытанием . Как вы, вероятно, знаете, вы не можете создать экземпляр абстрактного класса самостоятельно — его нужно расширять. Следовательно, если абстрактный класс не может быть создан, нет способа проверить его конкретные методы — те, которые наследуются всеми подклассами. Двойной тест можно использовать для создания поддельной версии расширенного абстрактного класса, который затем используется для тестирования только конкретных методов абстрактного класса. Лучше всего показать вам на примере. Давайте предположим, что у нашего абстракта API будет метод setTimeout используемый для установки времени ожидания запроса API на стороне Diffbot. Также предположим, что любое число от 0 до max int является действительным. По-настоящему TDD, давайте tests/Abstracts/ApiTest.php файл tests/Abstracts/ApiTest.php с содержимым:

 <?php namespace Swader\Diffbot\Test; use Swader\Diffbot\Abstracts\Api; class ApiTest extends \PHPUnit_Framework_TestCase { /** * @return \PHPUnit_Framework_MockObject_MockObject */ private function buildMock() { return $this->getMockForAbstractClass('Swader\Diffbot\Abstracts\Api'); } public function validTimeouts() { return [ 'zero' => [0], '1000' => [1000], '2000' => [2000], '3000' => [3000], '3 mil' => [3000000], '40 mil' => [40000000] ]; } public function invalidTimeouts() { return [ 'negative_big' => [-298979879827], 'negative_small' => [-4983], 'string ' => ['abcef'], 'empty string' => [''], 'bool' => [false] ]; } public function testSetEmptyTimeoutSuccess() { /** @var Api $mock */ $mock = $this->buildMock(); $mock->setTimeout(); } /** * @dataProvider invalidTimeouts */ public function testSetTimeoutInvalid($timeout) { /** @var Api $mock */ $mock = $this->buildMock(); $this->setExpectedException('InvalidArgumentException'); $mock->setTimeout($timeout); } /** * @dataProvider validTimeouts */ public function testSetTimeoutValid($timeout) { /** @var Api $mock */ $mock = $this->buildMock(); $mock->setTimeout($timeout); } } 

Мы определяем некоторых поставщиков данных, как в первом тестовом классе. Затем у нас есть метод для создания нашего макета, поэтому мы можем вызывать его при необходимости. Затем мы создаем тестовый метод для каждого сценария — без аргумента времени ожидания, плохого аргумента и хорошего аргумента. Только плохое ожидает и исключение.

Если мы запустим этот тест сейчас, мы получим ошибку:

Это совсем не удивительно — ведь мы еще не добавили класс API! Создайте файл src/Abstracts/Api.php с содержанием:

 <?php namespace Swader\Diffbot\Abstracts; /** * Class Api * @package Swader\Diffbot\Abstracts */ abstract class Api { } 

Запуск теста теперь выдает новую ошибку:

Вау! Мы сломали PHPUnit! Шучу, у нас все хорошо. Он жалуется на отсутствие метода setTimeout() , который ожидается в тесте — предполагается, что макет должен иметь его. Давайте изменим Api.php .

 <?php namespace Swader\Diffbot\Abstracts; /** * Class Api * @package Swader\Diffbot\Abstracts */ abstract class Api { /** @var int Timeout value in ms - defaults to 30s if empty */ private $timeout = 30000; public function setTimeout($timeout = null) { $this->timeout = $timeout; } } 

Повторно выполняя тест, мы получаем:

Теперь мы куда-то добираемся. Давайте сделаем одну окончательную реализацию нашей желаемой функциональности. Мы редактируем тело метода setTimeout следующим образом:

 /** * Setting the timeout will define how long Diffbot will keep trying * to fetch the API results. A timeout can happen for various reasons, from * Diffbot's failure, to the site being crawled being exceptionally slow, and more. * * @param int|null $timeout Defaults to 30000 even if not set * * @return $this */ public function setTimeout($timeout = null) { if ($timeout === null) { $timeout = 30000; } if (!is_int($timeout)) { throw new \InvalidArgumentException('Parameter is not an integer'); } if ($timeout < 0) { throw new \InvalidArgumentException('Parameter is negative. Only positive timeouts accepted.'); } $this->timeout = $timeout; return $this; } 

Наряду с логикой мы добавили докблок и заставили функцию возвращать экземпляр класса, который мы используем, чтобы мы могли связывать методы. Повторно запустив тесты, все должно пройти. Фактически, если мы посмотрим на отчет о покрытии, мы должны быть на 100% зелеными.

Вывод

Во второй части мы начали наше приключение с TDD, представив PHPUnit и использовав его для разработки некоторых функциональных возможностей нашего пакета. Вы можете скачать полный код части 2 (включая код части 1) из этой ветки . В следующей части мы продолжим сборку пакета, используя методы, описанные здесь, и добавим новый аспект — манипулирование данными. Будьте на связи!