В предыдущем посте мы рассмотрели SparkPost (как альтернативу Mandrill ) и немного изучили официальный PHP-клиент. Официальный клиент выполняет приличный объем работы, но я задумался над тем, что потребуется для создания нового клиента.
Чем больше я об этом думал, тем больше в этом смысла. Я мог узнать об API SparkPost и одновременно практиковать разработку через тестирование. Итак, в этом посте мы постараемся сделать именно это!
Вы можете найти код для этого поста на Github .
Начиная
Для начала нам понадобится Guzzle для отправки запросов в API SparkPost. Мы можем установить его с:
composer require guzzlehttp/guzzle
Кроме того, мы собираемся писать тесты раньше, поэтому мы также должны установить PHPUnit и Mockery:
composer require --dev phpunit/phpunit mockery/mockery
Прежде чем мы сможем запустить PHPUnit, нам нужно создать файл конфигурации:
<?xml version="1.0" encoding="UTF-8"?> <phpunit backupGlobals="false" backupStaticAttributes="false" bootstrap="vendor/autoload.php" colors="true" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="false" processIsolation="false" stopOnFailure="false" syntaxCheck="false"> <testsuites> <testsuite> <directory suffix="Test.php">tests</directory> </testsuite> </testsuites> <filter> <whitelist addUncoveredFilesFromWhitelist="true"> <directory suffix=".php">src</directory> </whitelist> </filter> </phpunit>
во<?xml version="1.0" encoding="UTF-8"?> <phpunit backupGlobals="false" backupStaticAttributes="false" bootstrap="vendor/autoload.php" colors="true" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="false" processIsolation="false" stopOnFailure="false" syntaxCheck="false"> <testsuites> <testsuite> <directory suffix="Test.php">tests</directory> </testsuite> </testsuites> <filter> <whitelist addUncoveredFilesFromWhitelist="true"> <directory suffix=".php">src</directory> </whitelist> </filter> </phpunit>
во<?xml version="1.0" encoding="UTF-8"?> <phpunit backupGlobals="false" backupStaticAttributes="false" bootstrap="vendor/autoload.php" colors="true" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="false" processIsolation="false" stopOnFailure="false" syntaxCheck="false"> <testsuites> <testsuite> <directory suffix="Test.php">tests</directory> </testsuite> </testsuites> <filter> <whitelist addUncoveredFilesFromWhitelist="true"> <directory suffix=".php">src</directory> </whitelist> </filter> </phpunit>
Этот файл конфигурации обрабатывает несколько вещей:
- Многие атрибуты корневого узла являются разумными, интуитивно понятными значениями по умолчанию. Единственное, на что я хочу обратить особое внимание — это
bootstrap
: он говорит PHPUnit загружать код автозагрузки Composer. - Мы говорим PHPUnit загружать все файлы, заканчивающиеся в
Test.php
, в папкеtests
. Он будет обрабатывать все файлы с этим суффиксом, как если бы они были файлами классов с одним классом каждый. Если он не может создать экземпляр какого-либо из найденных классов (например, абстрактных классов), он просто проигнорирует их. - Мы говорим PHPUnit добавить все файлы PHP (из папки
src
) в отчет о покрытии кода. Если вы не уверены, что это такое, не волнуйтесь. Мы посмотрим на это немного …
Теперь мы можем запустить:
vendor/bin/phpunit
… и мы должны увидеть:
Разработка интерфейса
Одна из вещей, которые мне больше всего нравятся в Test Driven Development, это то, как она подталкивает меня к интерфейсам, которые являются минималистичными и дружественными. Я начинаю как потребитель и заканчиваю реализацией, которую мне проще всего использовать.
Давайте сделаем наш первый тест. Самое простое, что мы можем сделать, это отправить электронное письмо через API SparkPost. Если вы посмотрите документы, вы обнаружите, что это происходит с помощью запроса POST
к https://api.sparkpost.com/api/v1/transmissions
с телом JSON и некоторыми ключевыми заголовками. Мы можем смоделировать это с помощью следующего кода:
require "vendor/autoload.php"; $client = new GuzzleHttp\Client(); $method = "POST"; $endpoint = "https://api.sparkpost.com/api/v1/transmissions"; $config = require "config.php"; $response = $client->request($method, $endpoint, [ "headers" => [ "Content-type" => "application/json", "Authorization" => $config["key"], ], "body" => json_encode([ "recipients" => [ [ "address" => [ "name" => "Christopher", "email" => "[email protected]", ], ], ], "content" => [ "from" => "[email protected]", "subject" => "The email subject", "html" => "The email <strong>content</strong>", ], ]), ]);
Вы можете использовать те же данные от / получателей, что и в предыдущем посте.
Предполагается, что у вас есть файл config.php
, в котором хранится ваш ключ API SparkPost:
return [ "key" => "[your SparkPost API key here]", ];
Обязательно добавьте этот файл в .gitignore
чтобы случайно не зафиксировать свой ключ API SparkPost в Github.
Учитывая грубые детали реализации, мы можем начать делать некоторые предположения (или определять некоторые требования) для открытого интерфейса:
- Мы не хотим повторять
https://api.sparkpost.com/api/v1
для каждого запроса. Мы могли бы принять это как аргумент конструктора. - Мы не хотим знать конкретные методы запроса или конечные точки. Клиент должен работать над этим и настраиваться под новые версии API.
- Мы можем захотеть определить некоторые или все параметры конечной точки, но должны быть некоторые разумные значения по умолчанию и способ для клиента переставить их.
Начиная с тестов
Поскольку мы хотим использовать Mockery, для нас было бы неплохо создать центральный базовый тестовый класс, чтобы закрыть макеты:
namespace SparkPost\Api\Test; use Mockery; use PHPUnit_Framework_TestCase; abstract class AbstractTest extends PHPUnit_Framework_TestCase { /** * @inheritdoc */ public function tearDown() { Mockery::close(); } }
Мы можем использовать это как базовый класс для других наших тестов, и PHPUnit не будет пытаться создать его экземпляр, поскольку он абстрактный. Основные причины закрытия Mockery немного выходят за рамки этого поста. Это просто то, что вам нужно знать, чтобы делать с Mockery (и аналогичными библиотеками).
Теперь мы можем определить, как должен выглядеть интерфейс клиента:
namespace SparkPost\Api\Test; use Mockery; use Mockery\MockInterface; use GuzzleHttp\Client as GuzzleClient; class ClientTest extends AbstractTest { /** * @test */ public function itCreatesTransmissions() { /** @var MockInterface|GuzzleClient $mock */ $mock = Mockery::mock(GuzzleClient::class); // ...what do we want to test here? } }
Идея Mockery заключается в том, что он позволяет нам моделировать классы, которые мы не хотим тестировать, которые требуются тем, кого мы хотим тестировать. В этом случае мы не хотим писать тесты для Guzzle (или даже делать с ним реальные HTTP-запросы), поэтому мы создаем макет класса Guzzle \ Client. Мы можем указать макету ожидать вызова метода request
со строкой метода запроса, строкой конечной точки и массивом опций:
$mock ->shouldReceive("request") ->with( Mockery::type("string"), Mockery::type("string"), $sendThroughGuzzle ) ->andReturn($mock);
Нам не важно, какой метод запроса отправляется в Guzzle или какой конечной точке. Они могут измениться при изменении API SparkPost, но важно то, что для этих аргументов отправляются строки. Что нас действительно интересует, так это то, правильно ли отформатированы параметры, которые мы отправляем клиенту.
Допустим, мы хотели проверить следующие данные:
$sendThroughGuzzle = [ "headers" => [ "Content-type" => "application/json", "Authorization" => "[fake SparkPost API key here]", ], "body" => json_encode([ "recipients" => [ [ "address" => [ "name" => "Christopher", "email" => "[email protected]", ], ], ], "content" => [ "from" => "[email protected]", "subject" => "The email subject", "html" => "The email <strong>content</strong>", ], ]), ];
… но мы хотели, чтобы эти данные проходили, только если мы вызвали следующий метод:
$client = new SparkPost\Api\Client( $mock, "[fake SparkPost API key here]" ); $client->createTransmission([ "recipients" => [ [ "address" => [ "name" => "Christopher", "email" => "[email protected]", ], ], ], "subject" => "The email subject", "html" => "The email <strong>content</strong>", ]);
Тогда нам, вероятно, нужно будет заставить остальную часть теста работать с этой структурой. Давайте предположим, что нас интересуют json_decode
данные json_decode
возвращаемые из SparkPost…
Мы могли бы представить, используя клиента, как:
- Мы вызываем
$client->createTransmission(...)
. - Клиент форматирует эти параметры в соответствии с тем, как их использует SparkPost.
- Клиент отправляет аутентифицированный запрос в SparkPost.
- Ответ отправляется через
json_decode
и просто получает массив данных ответа.
Мы можем встроить эти шаги в тест:
public function itCreatesTransmissions() { /** @var MockInterface|GuzzleClient $mock */ $mock = Mockery::mock(GuzzleClient::class); $sendThroughGuzzle = [ "headers" => [ "Content-type" => "application/json", "Authorization" => "[fake SparkPost API key here]", ], "body" => json_encode([ "recipients" => [ [ "address" => [ "name" => "Christopher", "email" => "[email protected]", ], ], ], "content" => [ "from" => "[email protected]", "subject" => "The email subject", "html" => "The email <strong>content</strong>", ], ]), ]; $mock ->shouldReceive("request") ->with( Mockery::type("string"), Mockery::type("string"), $sendThroughGuzzle ) ->andReturn($mock); $mock ->shouldReceive("getBody") ->andReturn( json_encode(["foo" => "bar"]) ); $client = new Client( $mock, "[fake SparkPost API key here]" ); $this->assertEquals( ["foo" => "bar"], $client->createTransmission([ "recipients" => [ [ "address" => [ "name" => "Christopher", "email" => "[email protected]", ], ], ], "subject" => "The email subject", "html" => "The email <strong>content</strong>", ]) ); }
Прежде чем мы сможем запустить PHPUnit, чтобы увидеть этот тест в действии, нам нужно добавить директивы автозагрузки для нашей библиотеки, в composer.json
:
"autoload": { "psr-4": { "SparkPost\\Api\\": "src" } }, "autoload-dev": { "psr-4": { "SparkPost\\Api\\Test\\": "tests" } }
Вам, вероятно, также придется запустить composer du
чтобы заставить их работать …
Реализация интерфейса
В тот момент, когда мы запустим PHPUnit, мы увидим все виды ошибок. Мы еще не сделали классы, не говоря уже о реализации их поведения. Давайте сделаем класс Client
:
<?php namespace SparkPost\Api; use GuzzleHttp\Client as GuzzleClient; class Client { /** * @var string */ private $base = "https://api.sparkpost.com/api/v1"; /** * @var GuzzleClient */ private $client; /** * @var string */ private $key; /** * @param GuzzleClient $client * @param string $key */ public function __construct(GuzzleClient $client, $key) { $this->client = $client; $this->key = $key; } /** * @param array $options * * @return array */ public function createTransmission(array $options = []) { $options = array_replace_recursive([ "from" => "[email protected]", ], $options); $send = [ "recipients" => $options["recipients"], "content" => [ "from" => $options["from"], "subject" => $options["subject"], "html" => $options["html"], ] ]; return $this->request("POST", "transmissions", $send); } /** * @param string $method * @param string $endpoint * @param array $options * * @return array */ private function request( $method, $endpoint, array $options = [] ) { // ...time to forward the request } }
Мы делаем успехи! Когда мы запускаем PHPUnit, мы должны увидеть что-то вроде:
Давайте закончим, перенаправив запросы в SparkPost:
private function request( $method, $endpoint, array $options = [] ) { $endpoint = $this->base . "/" . $endpoint; $response = $this->client->request($method, endpoint, [ "headers" => [ "Content-type" => "application/json", "Authorization" => $this->key, ], "body" => json_encode($options), ]); return json_decode($response->getBody(), true); }
При этом тесты будут проходить.
Возможно, нам также полезно увидеть, как это можно использовать без насмешек:
require "vendor/autoload.php"; $config = require "config.php"; $client = new SparkPost\Api\Client( new GuzzleHttp\Client(), $config["key"] ); $reponse = $client->createTransmission([ "recipients" => [ [ "address" => [ "name" => "Christopher", "email" => "[email protected]", ], ], ], "subject" => "The email subject", "html" => "The email <strong>content</strong>", ]);
покрытие
Помните, я упоминал отчет о покрытии кода? Мы можем запустить PHPUnit следующим образом:
vendor/bin/phpunit --coverage-html coverage
Это создаст папку файлов HTML. Вы можете открыть coverage/index.html
чтобы получить представление о том, какая часть вашей библиотеки покрыта вашими тестами. Вам нужно будет установить расширение XDebug, и запуск больших наборов тестов с этим параметром может быть медленнее, чем без него.
Что нужно учитывать
-
Мы не проверяли параметры для
createTransmission
. Это было бы хорошо сделать. -
Мы очень тесно связаны с Guzzle. Я думаю, что это нормально, но если у вас есть резервирование, лучше создайте «клиентский» интерфейс и адаптер Guzzle, или, что еще лучше, используйте подход Httplug .
-
В SparkPost API есть намного больше, и есть несколько интересных путей, которые вы можете выбрать при разработке такого интерфейса. Вот недавний скринкаст, который я сделал, который исследует выразительный синтаксис поверх клиента, который мы создали сегодня…
Как вы находите SparkPost? Вам нравится такой процесс разработки? Дайте нам знать в комментариях ниже.