用GuzzlePHP进行单元测试

In January, Miguel Romero wrote a great article showing how to get started with Guzzle. If you missed the article, or are unfamiliar with Guzzle:

一月,Miguel Romero 写了一篇很棒的文章,展示了如何开始使用Guzzle。 如果您错过了这篇文章,或者对Guzzle不熟悉:

Guzzle is a PHP HTTP client & framework for building RESTful web service clients.

Guzzle是用于构建RESTful Web服务客户端PHP HTTP客户端和框架。

In Miguel’s article, he covered a range of the library’s features, including basic configuration for GET and POST requests, plugins, such as logging using monolog, and finished up with interacting with an OAuth service, specifically GitHub’s API.

在Miguel的文章中,他涵盖了库的一系列功能,包括GET和POST请求的基本配置,插件(例如,使用monolog进行日志记录)以及与OAuth服务(尤其是GitHub的API)进行交互的过程。

In this tutorial, I want to show you how to use Guzzle from a different perspective, specifically how to do unit testing with it. To do this, we’re going to look at three specific approaches:

在本教程中,我想向您展示如何从另一个角度使用Guzzle,特别是如何使用它进行单元测试。 为此,我们将研究三种特定的方法:

  1. Hand Crafting Custom Responses

    手工制作自定义回复
  2. Using a ServiceClient with Mock Response Files

    将ServiceClient与模拟响应文件一起使用
  3. Enqueueing a Server with Mock Responses

    使具有模拟响应的服务器入队

设定 (Getting Set Up)

Note: The source code for this article is available on Github.

注意:本文的源代码在Github上可用。

Like all things, we need to walk before we can run. In this case, we need to set up our test environment and test class. To get everything in place, we’ll be using Composer. If you’re not familiar with Composer, please read Alexander’s article here on SitePoint before continuing.

像所有事物一样,我们需要走路才能跑步。 在这种情况下,我们需要设置测试环境和测试类。 为了使一切就绪,我们将使用Composer。 如果您不熟悉Composer,请先在SitePoint上阅读Alexander的文章 ,然后再继续。

Our composer.json file will look as follows:

我们的composer.json文件将如下所示:

{
    "require": {
        "php": ">=5.3.3",
    }
    "require-dev": {
        "phpunit/phpunit": "4.0.*",
        "guzzle/guzzle": "~3.7"
    }
}

I’ve stipulated a minimum PHP version of 5.3.3. To be fair, it likely should be higher, but this is a good start. Our only other requirements are PHPUnit and Guzzle. After adding these to composer.json, in your project run composer install and after a short wait, the dependencies will be ready to go.

我规定了最低PHP版本5.3.3。 公平地说,可能应该更高,但这是一个好的开始。 我们唯一的其他要求是PHPUnit和Guzzle。 将它们添加到composer.json后,在您的项目中运行composer install并稍等片刻,即可开始使用依赖项。

准备PHPUnit (Preparing PHPUnit)

Before we can run our unit tests, we need to do a bit of preparation there as well. First, create a directory in your project called tests. In there, create two files: bootstrap.php and phpunit.xml.dist.

在运行单元测试之前,我们还需要在那里做一些准备。 首先,在您的项目中创建一个名为tests的目录。 在其中创建两个文件: bootstrap.phpphpunit.xml.dist

bootstrap.php is quite simple:

bootstrap.php很简单:

<?php
error_reporting(E_ALL | E_STRICT);
require dirname(__DIR__) . '/vendor/autoload.php';

This includes the auto-generated autoload.php file from the vendor directory, which Composer created. It ensures we have access to both PHPUnit and Guzzle. Next, let’s look at phpunit.xml.dist.

这包括由Composer创建的供应商目录中自动生成的autoload.php文件。 它确保我们可以访问PHPUnit和Guzzle。 接下来,让我们看一下phpunit.xml.dist

<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="./bootstrap.php" colors="true">
    <testsuites>
        <testsuite name="importer-tests">
            <directory suffix="Test.php">./</directory>
        </testsuite>
    </testsuites>
</phpunit>

This is a rather rudimentary configuration. What it does is:

这是一个非常基本的配置。 它的作用是:

  • Tell PHPUnit to use bootstrap.php to bootstrap the test environment

    告诉PHPUnit使用bootstrap.php引导测试环境

  • Use colors in the test output (handy for discerning the import aspects)

    在测试输出中使用颜色(便于识别导入方面)
  • Set up one testsuite, called tests. This looks for tests in all files that end in Test.php, located anywhere under the current directory

    设置一个测试套件,称为tests 。 这将在以Test.php结尾的所有文件中查找测试,该文件位于当前目录下的任何位置

There is quite an array of available options and configurations, but this suits our needs. If you’re interested, check out the configuration documentation for more details.

有很多可用的选项和配置,但这符合我们的需求。 如果您有兴趣,请查看配置文档以获取更多详细信息。

With that done, let’s start building our test class. In the tests directory, create a new file called SitePointGuzzleTest.php. In it, add the following:

完成之后,让我们开始构建测试类。 在测试目录中,创建一个名为SitePointGuzzleTest.php的新文件。 在其中添加以下内容:

<?php
use Guzzle\Tests\GuzzleTestCase,
    Guzzle\Plugin\Mock\MockPlugin,
    Guzzle\Http\Message\Response,
    Guzzle\Http\Client as HttpClient,
    Guzzle\Service\Client as ServiceClient,
    Guzzle\Http\EntityBody;

class SitePointGuzzleTest extends GuzzleTestCase 
{
    protected $_client;
}

Here, we’ve imported the key classes which our test class needs. Our class extends from GuzzleTestCase, giving it access to some of the Guzzle-specific test functionality we’ll be using. So far, so good. Let’s look at custom responses.

在这里,我们导入了测试类所需的关键类。 我们的类从GuzzleTestCase扩展而来,使它可以访问我们将要使用的某些Guzzle特定的测试功能。 到目前为止,一切都很好。 让我们看看自定义响应。

手工制作自定义回复 (Hand Crafting Custom Responses)

From Miguel’s article, you’ll be familiar with initializing a client to make a request, potentially passing in parameters, then inspecting the response. Let’s assume you’ve created a class which either uses or wraps a Guzzle client, or uses a Guzzle Response object.

在Miguel的文章中,您将熟悉初始化客户端以发出请求,可能传递参数然后检查响应的过程。 假设您创建了一个使用或包装Guzzle客户端或使用Guzzle Response对象的类。

And let’s say you’re working with the FreeAgent API and you want to test code which retrieves invoice data.

假设您正在使用FreeAgent API,并且想要测试检索发票数据的代码。

You want to be sure that your code reacts as required, should something go wrong or the API change. Let’s look at how to mock a response, by going through some annotated code.

如果出现问题或API发生更改,您希望确保您的代码能够按要求做出React。 让我们看一下如何通过一些带注释的代码来模拟响应。

public function testAnotherRequest() {
    $mockResponse = new Response(200);
    $mockResponseBody = EntityBody::factory(fopen(
        './mock/bodies/body1.txt', 'r+')
    );
    $mockResponse->setBody($mockResponseBody);
    // ...
}

Here we first instantiate a new Response object. We then use the factory method of Guzzle\Http\EntityBody to set the response body with the contents of the file ./mock/bodies/body1.txt.

在这里,我们首先实例化一个新的Response对象。 然后,我们使用Guzzle\Http\EntityBody的工厂方法使用文件./mock/bodies/body1.txt的内容设置响应正文。

This both makes it easy to separate configuration from code and makes test maintenance simpler. body1.txt is available in the source code for this article.

这既使配置与代码分离变得容易,又使测试维护更加简单。 body1.txt的源代码中提供了body1.txt

$mockResponse->setHeaders(array(
    "Host" => "httpbin.org",
    "User-Agent" => "curl/7.19.7 (universal-apple-darwin10.0) libcurl/7.19.7 OpenSSL/0.9.8l zlib/1.2.3",
    "Accept" => "application/json",
    "Content-Type" => "application/json"
));

We then pass an associative array to setHeaders, which will set four custom headers on our response.

然后,我们将一个关联数组传递给setHeaders,这将在响应中设置四个自定义标头。

$plugin = new MockPlugin();
$plugin->addResponse($mockResponse);
$client = new HttpClient();
$client->addSubscriber($plugin);

Here we create a new MockPlugin object, passing in the response object to it. Once we instantiate our new HttpClient, we add the MockPlugin as a subscriber, which in turn will ensure requests made with it use the mock response we’ve just created.

在这里,我们创建一个新的MockPlugin对象,并将响应对象传递给它。 实例化新的HttpClient ,我们将MockPlugin添加为订阅者,这将确保与此相关的请求使用我们刚刚创建的模拟响应。

$request = $client->get(
    'https://api.freeagent.com/v2/invoices'
);
$response = $request->send();

Now, as in Miguel’s article, we call get on the client, and send on the returned request to get a response object. With that, we can run a series of test assertions.

现在,就像在Miguel的文章中一样,我们在客户端上调用get,然后发送返回的请求以获取响应对象。 这样,我们可以运行一系列测试断言。

$this->assertEquals(200, $response->getStatusCode());
$this->assertTrue(in_array(
    'Host', array_keys($response->getHeaders()->toArray())
));
$this->assertTrue($response->hasHeader("User-Agent"));
$this->assertCount(4, $response->getHeaders());
$this->assertSame(
    $mockResponseBody->getSize(), 
    $response->getBody()->getSize()
);
$this->assertSame(
    1, 
    count(json_decode($response->getBody(true))->invoices
));

In the assertions, you can see that I’ve checked the response code, if Host and User-Agent were in the response headers, the number of headers sent, that the size of the body matched the size of our faked response body and that there was one invoice in the response received.

在断言中,您可以看到我检查了响应代码,如果HostUser-Agent在响应标头中,发送的标头数,主体的大小与伪造的响应主体的大小匹配以及收到的回复中有一张发票。

This is just a small sample of the kinds of tests which can be run, but it shows just how easy it is to both mock a custom response and to test it when it’s retrieved. What other kinds of tests would you run?

这只是可以运行的各种测试的一小部分,但它显示了模拟自定义响应并在检索到响应时对其进行测试是多么容易。 您还将运行其他哪些测试?

将ServiceClient与模拟响应文件一起使用 (Using a ServiceClient with Mock Response Files)

That was the long way to mock up a response. If you remember, I emphasised at the start that our test class extends GuzzleTestCase, to get access to some excellent testing functionality. Let’s look at how we can skip a lot of the work we just did by using it.

那是模拟回应的漫长之路。 如果您还记得的话,我一开始就强调我们的测试类扩展了GuzzleTestCase,以获取一些出色的测试功能。 让我们看一下如何通过使用它跳过很多我们刚刚完成的工作。

This time, let’s override setUp as follows:

这次,让我们如下重写setUp

public function setUp()
{
    $this->_client = new ServiceClient();
    $this->setMockBasePath('./mock/responses');
    $this->setMockResponse(
        $this->_client, array('response1')
    );
}

Here, we’ve instantiated the class variable $_client as a new ServiceClient. We’ve then used setMockBasePath, to ./mock/responses, and called setMockResponse, passing in our client object and an array.

在这里,我们将类变量$_client实例$_client新的ServiceClient 。 然后,我们将setMockBasePath应用于./mock/responses ,并调用setMockResponse ,以传入我们的客户端对象和一个数组。

The array lists the names of files in ‘./mock/responses’. The contents of these files will be used to set up a series of responses, which the client will receive for each successive call to send.

该数组在“ ./mock/responses”中列出文件名。 这些文件的内容将用于设置一系列响应,客户端将在每次连续的send调用中收到该响应。

In this case, I’ve only added one, but you could easily add as many as you like. You can see in ./mock/responses/response1, that it lists the HTTP version, status code, headers and response body. Once again, it keeps the code and configuration neatly separate.

在这种情况下,我只添加了一个,但是您可以轻松添加任意多个。 您可以在./mock/responses/response1中看到它列出了HTTP版本,状态代码,标头和响应正文。 再次,它使代码和配置完全分开。

Now, let’s look at the function which uses it.

现在,让我们看一下使用它的函数。

public function testRequests() {
    $request = $this->_client->get(
        'https://api.freeagent.com/v2/invoices'
    );
    $request->getQuery()->set(
        'view', 'recent_open_or_overdue'
    );
    $response = $request->send();

    $this->assertContainsOnly(
        $request, $this->getMockedRequests()
    );
    $this->assertEquals(200, $response->getStatusCode());
    $this->assertEquals(
        'AmazonS3', $response->getServer()
    );
    $this->assertEquals(
        'application/xml', $response->getContentType()
    );
}

You can see that all I’ve had to do is to make a call to get on the client object and send on the returned request object, as we normally would. I’ve added in query parameters just for good measure.

您可以看到,我要做的就是像通常那样,通过调用来get客户端对象并发送返回的请求对象。 我添加了查询参数只是为了很好。

As before, I’ve then been able to run a series of assertions, checking the mocked requests, returned status code, server and content-type header.

和以前一样,我现在能够运行一系列断言,检查模拟的请求,返回的状态代码,服务器和内容类型标头。

One thing worth mentioning is that it’s a FIFO queue; meaning that the first response added is the first which will be sent. Don’t let this trip you up.

值得一提的是,这是一个FIFO队列 。 表示添加的第一个响应是将发送的第一个响应。 不要让这个绊倒你。

使具有模拟响应的服务器入队 (Enqueueing a Server with Mock Responses)

Finally, let’s look at one of the cooler aspects of Guzzle. If we call $this->getServer()->enqueue(array());, Guzzle transparently starts a node.js server behind the scenes. We can then use that to send requests to, as if it was our real server endpoint. Let’s have a look at how to use it.

最后,让我们看一下Guzzle较酷的方面之一。 如果我们调用$this->getServer()->enqueue(array()); ,Guzzle在后台透明地启动了一个node.js服务器。 然后,我们可以使用它来发送请求,就像它是我们的真实服务器端点一样。 让我们看看如何使用它。

public function testWithRemoteServer() {
    $mockProperties = array(
        array(
            'header' => './mock/headers/header1.txt',
            'body' => './mock/bodies/body1.txt',
            'status' => 200
        )
    );
}

Here I create an array to store header, body, and status information for a mock request, specifying a status code of 200, and files containing the header and body response data.

在这里,我创建了一个数组来存储模拟请求的标头,正文和状态信息,并指定状态码200,以及包含标头和正文响应数据的文件。

$mockResponses = array();

foreach($mockProperties as $property) {
    $mockResponse = new Response($property['status']);
    $mockResponseBody = EntityBody::factory(
        fopen($property['body'], 'r+')
    );
    $mockResponse->setBody($mockResponseBody);
    $headers = explode(
        "\n", 
        file_get_contents($property['header'], true)
    );
    foreach($headers as $header) {
        list($key, $value) = explode(': ', $header);
        $mockResponse->addHeader($key, $value);
    }
    $mockResponses[] = $mockResponse;
}

Then I’ve created a new Response object, setting the status code, and again used the factory method of the EntityBody class to set the body. The headers were a bit more cumbersome, so I’ve iterated over the contents of the file, calling addHeader for each key/value pair retrieved.

然后,我创建了一个新的Response对象,设置了状态代码,并再次使用EntityBody类的factory方法设置了正文。 标头有点麻烦,因此我遍历了文件的内容,为检索到的每个键/值对调用addHeader

$this->getServer()->enqueue($mockResponses);

Each mock response object created is added to an array, which is then passed to enqueue. Now it has a set of responses ready to be sent to our client requests.

创建的每个模拟响应对象都添加到数组中,然后传递给enqueue 。 现在,它具有一组准备发送给我们客户请求的响应。

$client = new HttpClient();
$client->setBaseUrl($this->getServer()->getUrl());
$request = $client->get();
$request->getQuery()->set(
    'view', 'recent_open_or_overdue'
);
$response = $request->send();

Here, as before, we’ve initialized our client object, calling get and send on the returned request. The one thing to note, is that to use the Node.JS server, we need to pass $this->getServer()->getUrl() to $client->setBaseUrl(), otherwise it won’t work.

在这里,和以前一样,我们已经初始化了客户端对象,并在返回的请求上调用了get和send。 要注意的一件事是,要使用Node.JS服务器,我们需要将$this->getServer()->getUrl()传递给$client->setBaseUrl() ,否则它将无法正常工作。

$this->assertCount(5, $response->getHeaders());
$this->assertEmpty($response->getContentDisposition());
$this->assertSame('HTTP', $response->getProtocol());

Following that, our code works as it did before, and I’ve added assertions for the headers, content disposition header and the protocol in the returned response object.

之后,我们的代码将像以前一样工作,并且我在返回的响应对象中为标头,内容处置标头和协议添加了断言。

结语 (Wrapping Up)

So, what do you think? Though it does take a bit of work, I’m sure you can see how simple it is to set up the different response approaches. I’m sure with a little work, you could also simplify the approaches I’ve taken.

所以你怎么看? 尽管确实需要一些工作,但是我敢肯定,您会发现设置不同的响应方法非常简单。 我敢肯定,通过一些工作,您还可以简化我采用的方法。

I know that this only scratched the surface of what’s possible with testing in Guzzle, but I hope I piqued your curiosity, showing you a wealth of ways to ensure that your client code is fully test covered.

我知道这只是在Guzzle中进行测试所可能实现的一切,但我希望我引起您的好奇心,向您展示了确保您的客户端代码完全受测试覆盖的大量方法。

What’s your experience? Share your thoughts in the comments.

您的经验是什么? 在评论区分享你的观点。

翻译自: https://www.sitepoint.com/unit-testing-guzzlephp/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值