phpunit_构建一个SparkPost客户端:带有PhpUnit和Mockery的TDD

phpunit

In a previous post, we looked at SparkPost (as an alternative to Mandrill), and explored a bit of the official PHP client. The official client handles a decent amount of work, but I got to thinking about what it would take to build a new client.

在上一篇文章中 ,我们研究了SparkPost (作为Mandrill的替代品),并探索了一些正式PHP客户端。 正式客户处理大量工作,但我必须考虑如何构建新客户。

The more I thought about it, the more it made sense. I could learn about the SparkPost API, and practice Test Driven Development at the same time. So, in this post we’ll look to do just that!

我想得越多,它就越有意义。 我可以了解SparkPost API,并同时练习测试驱动开发。 因此,在这篇文章中,我们将期待做到这一点!

Home page screenshot

You can find the code for this post on Github.

您可以在Github上找到此帖子的代码。

入门 (Getting Started)

To begin, we’re going to need Guzzle to make requests to the SparkPost API. We can install it with:

首先,我们将需要Guzzle向SparkPost API发出请求。 我们可以通过以下方式安装它:

composer require guzzlehttp/guzzle

In addition, we’re going to be writing tests early, so we should also install PHPUnit and Mockery:

另外,我们将尽早编写测试,因此我们还应该安装PHPUnit和Mockery:

composer require --dev phpunit/phpunit mockery/mockery

Before we can run PHPUnit, we need to create a configuration file:

在运行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>

This configuration file handles a number of things:

此配置文件处理许多事情:

  1. Many of the root node attributes are sensible, intuitive defaults. The one I want to draw particular attention to is bootstrap: which tells PHPUnit to load Composer’s autoload code.

    许多根节点属性是明智的,直观的默认值。 我要特别注意的是bootstrap :它告诉PHPUnit加载Composer的自动加载代码。

  2. We tell PHPUnit to load all files ending in Test.php, in the tests folder. It will treat all files with this suffix as though they are class files with a single class each. If it can’t instantiate any of the classes it finds (like abstract classes) then it will just ignore those.

    我们告诉PHPUnit将所有以Test.php结尾的文件加载到tests文件夹中。 它将把所有带有此后缀的文件视为它们是每个具有单个类的类文件。 如果它无法实例化它找到的任何类(例如抽象类),那么它将忽略这些类。

  3. We tell PHPUnit to add all PHP files (from the src folder) to code coverage reporting. If you’re unsure what that is, don’t worry. We’ll look at it in a bit…

    我们告诉PHPUnit将所有PHP文件(来自src文件夹)添加到代码覆盖率报告中。 如果不确定那是什么,不用担心。 我们待会儿再看...

We can now run:

我们现在可以运行:

vendor/bin/phpunit

… and we should see:

……我们应该看到:

New phpunit screen

设计界面 (Designing The Interface)

One of the things I love most about Test Driven Development is how it pushes me towards interfaces that are minimalistic and friendly. I start as the consumer, and end up with an implementation that is the easiest for me to use.

我最喜欢“测试驱动开发”的一件事是它如何将我推向简约而友好的界面。 我从消费者开始,最后得到了最容易使用的实现。

Let’s make our first test. The simplest thing we can do is send an email through the SparkPost API. If you check out the docs, you’ll find that this happens through a POST request to https://api.sparkpost.com/api/v1/transmissions, with a JSON body and some key headers. We can simulate this with the following code:

让我们进行第一个测试。 我们可以做的最简单的事情就是通过SparkPost API发送电子邮件。 如果您查看文档,则会发现这是通过对JSON正文和一些关键标头的POST请求https://api.sparkpost.com/api/v1/transmissions发生的。 我们可以使用以下代码对此进行仿真:

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" => "cgpitt@gmail.com",
                ],
            ],
        ],
        "content" => [
            "from" => "sandbox@sparkpostbox.com",
            "subject" => "The email subject",
            "html" => "The email <strong>content</strong>",
        ],
    ]),
]);

You can use the same from/recipients details as in the previous post.

您可以使用与上一篇文章相同的“收件人/收件人”详细信息。

This assumes you have a config.php file, which stores your SparkPost API key:

假设您有一个config.php文件,该文件存储您的SparkPost API密钥:

return [
    "key" => "[your SparkPost API key here]",
];

Be sure to add this file to .gitignore so you don’t accidentally commit your SparkPost API key to Github.

确保将此文件添加到.gitignore以免意外将SparkPost API密钥提交给Github。

Given the rough details of implementation, we can start to make some assumptions (or define some requirements) for the public interface:

有了实现的粗略细节,我们就可以开始为公共接口做一些假设(或定义一些要求):

  1. We don’t want to repeat https://api.sparkpost.com/api/v1 for every request. We could accept it as a constructor argument.

    我们不想为每个请求重复https://api.sparkpost.com/api/v1 。 我们可以接受它作为构造函数参数。

  2. We don’t want to know the specific request methods or endpoints. The client should work that stuff out and adjust itself for new versions of the API.

    我们不想知道特定的请求方法或端点。 客户端应将这些工作填满,并针对API的新版本进行调整。
  3. We may want to define some or all of the endpoint parameters, but there should be some sensible defaults and a way for the client to rearrange them.

    我们可能要定义一些或所有端点参数,但是应该有一些合理的默认值以及客户端重新排列它们的方法。

从测试开始 (Starting With Tests)

Since we want to use Mockery, it would be a good idea for us to make a central, base test class, to close the mocks:

既然我们要使用Mockery,那么对我们来说,建立一个中心的基础测试类来关闭模拟是一个好主意:

namespace SparkPost\Api\Test;

use Mockery;
use PHPUnit_Framework_TestCase;

abstract class AbstractTest extends PHPUnit_Framework_TestCase
{
    /**
     * @inheritdoc
     */
    public function tearDown()
    {
        Mockery::close();
    }
}

We can use this as the base class for our other tests, and PHPUnit won’t try to instantiate it since it is abstract. The underlying reasons for closing Mockery are a bit out of scope for this post. It’s just something you need to know to do with Mockery (and similar libraries).

我们可以将其用作其他测试的基类,并且PHPUnit不会尝试实例化它,因为它是抽象的。 关闭Mockery的根本原因超出了本文的范围。 这只是与Mockery(和类似的库)相关的知识。

Now we can begin to define what we want the client interface to look like:

现在我们可以开始定义我们想要的客户端界面了:

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?
    }
}

The idea behind Mockery is that it allows us to simulate classes we don’t want to test, that are required by those we do want to test. In this case, we don’t want to write tests for Guzzle (or even make real HTTP requests with it) so we create a mock of the Guzzle\Client class. We can tell the mock to expect a call to the request method, with a request method string, an endpoint string and an array of options:

Mockery背后的想法是,它允许我们模拟我们不想测试的类,而这些类正是我们想要测试的类所必需的。 在这种情况下,我们不想为Guzzle编写测试(甚至不使用它进行真正的HTTP请求),因此我们创建了Guzzle \ Client类的模拟。 我们可以告诉模拟程序期望对request方法的调用,其中包含请求方法字符串,端点字符串和选项数组:

$mock
    ->shouldReceive("request")
    ->with(
        Mockery::type("string"),
        Mockery::type("string"),
        $sendThroughGuzzle
    )
    ->andReturn($mock);

We don’t really care which request method is sent to Guzzle, or which endpoint. Those might change as the SparkPost API changes, but the important thing is that strings are sent for these arguments. What we are really interested in is seeing is whether the parameters we send to the client are formatted correctly.

我们实际上并不关心哪个请求方法发送到Guzzle或哪个端点。 这些可能会随着SparkPost API的更改而更改,但重要的是要为这些参数发送字符串。 我们真正感兴趣的是查看我们发送给客户端的参数是否正确设置了格式。

Let’s say we wanted to test the following data:

假设我们要测试以下数据:

$sendThroughGuzzle = [
    "headers" => [
        "Content-type" => "application/json",
        "Authorization" => "[fake SparkPost API key here]",
    ],
    "body" => json_encode([
        "recipients" => [
            [
                "address" => [
                    "name" => "Christopher",
                    "email" => "cgpitt@gmail.com",
                ],
            ],
        ],
        "content" => [
            "from" => "sandbox@sparkpostbox.com",
            "subject" => "The email subject",
            "html" => "The email <strong>content</strong>",
        ],
    ]),
];

…but we only wanted that data to go through if we called the following method:

…但是我们只希望通过以下方法传递数据:

$client = new SparkPost\Api\Client(
    $mock, "[fake SparkPost API key here]"
);

$client->createTransmission([
    "recipients" => [
        [
            "address" => [
                "name" => "Christopher",
                "email" => "cgpitt@gmail.com",
            ],
        ],
    ],
    "subject" => "The email subject",
    "html" => "The email <strong>content</strong>",
]);

Then we would probably need to make the rest of the test work with this structure in mind. Let’s assume we’re only interested in the json_decode data returned from SparkPost…

然后,我们可能需要考虑此结构来进行其余的测试。 假设我们只对json_decode返回的json_decode数据感兴趣...

We could imagine using the client like:

我们可以想象像这样使用客户端:

  1. We call $client->createTransmission(...).

    我们称之为$client->createTransmission(...)

  2. The client formats those parameters according to how SparkPost wants them.

    客户端根据SparkPost想要的参数格式化这些参数。
  3. The client sends an authenticated request to SparkPost.

    客户端将已验证的请求发送到SparkPost。
  4. The response is sent through json_decode, and just gets an array of response data.

    响应通过json_decode发送,并且仅获取响应数据数组。

We can build these steps into the test:

我们可以将这些步骤构建到测试中:

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" => "cgpitt@gmail.com",
                    ],
                ],
            ],
            "content" => [
                "from" => "sandbox@sparkpostbox.com",
                "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" => "cgpitt@gmail.com",
                    ],
                ],
            ],
            "subject" => "The email subject",
            "html" => "The email <strong>content</strong>",
        ])
    );
}

Before we can run PHPUnit to see this test in action, we need to add the autoload directives for our library, to composer.json:

在运行PHPUnit观看此测试之前,我们需要将库的autoload指令添加到composer.json

"autoload": {
    "psr-4": {
        "SparkPost\\Api\\": "src"
    }
},
"autoload-dev": {
    "psr-4": {
        "SparkPost\\Api\\Test\\": "tests"
    }
}

You’ll probably also have to run composer du to get these to work…

您可能还必须运行composer du才能使它们工作……

实施接口 (Implementing The Interface)

The moment we run PHPUnit, we’ll see all sorts of errors. We’ve not yet made the classes, let alone implemented their behavior. Let’s make the Client class:

运行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" => "sandbox@sparkpostbox.com",
        ], $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
    }
}

We’re making progress! When we run PHPUnit we should see something like:

我们正在进步! 当我们运行PHPUnit时,我们应该看到类似以下内容:

Invalid return type error

Let’s finish up by forwarding the requests to SparkPost:

最后,将请求转发到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);
}

With this, the tests will be passing.

这样,测试将通过。

Passing tests

It’s perhaps also helpful for us to see how this can be consumed without any mocking:

看看如何在不进行任何模拟的情况下将其消耗掉也可能对我们有帮助:

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" => "cgpitt@gmail.com",
            ],
        ],
    ],
    "subject" => "The email subject",
    "html" => "The email <strong>content</strong>",
]);

覆盖范围 (Coverage)

Remember I mentioned code coverage reporting? We can run PHPUnit in the following way:

还记得我提到过代码覆盖率报告吗? 我们可以通过以下方式运行PHPUnit:

vendor/bin/phpunit --coverage-html coverage

This will generate a folder of HTML files. You can open coverage/index.html to get an idea of how much of your library is covered by your tests. You’ll need to have the XDebug extension installed, and running larger tests suites with this parameter may be slower than without.

这将生成HTML文件的文件夹。 您可以打开coverage/index.html来了解测试覆盖了多少库。 您需要安装XDebug扩展,并且使用此参数运行较大的测试套件可能会比不使用该套件慢。

Coverage report

要考虑的事情 (Things To Consider)

  1. We didn’t validate the parameters for createTransmission. That would be a good thing to do.

    我们没有验证createTransmission的参数。 那将是一件好事。

  2. We’re very tightly coupled to Guzzle. I think that’s ok, but if you have reservations, rather create a “client” interface and a Guzzle adapter, or even better, take the Httplug approach.

    我们与Guzzle紧密相连。 我认为可以,但是如果您有保留,请创建一个“客户端”界面和一个Guzzle适配器,甚至更好,请采用Httplug方法

  3. There is a lot more to the SparkPost API, and there are some interesting paths you can take when designing such an interface. Here’s a recent screencast I did which explores an expressive syntax on top of the client we’ve built today…

    SparkPost API还有很多,在设计这样的接口时可以采取一些有趣的途径。 这是我最近进行的截屏视频,它在我们今天构建的客户端之上探索了表达语法

How are you finding SparkPost? Do you like this sort of development flow? Let us know in the comments below.

您如何找到SparkPost? 您喜欢这种开发流程吗? 在下面的评论中让我们知道。

翻译自: https://www.sitepoint.com/building-a-sparkpost-client-tdd-with-phpunit-and-mockery/

phpunit

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值