使用 PHPUnit 在 Laravel 控制器内部进行单元测试

Unit Testing Guzzle inside of Laravel Controller with PHPUnit

我不太确定在这种情况下采用哪种方法进行单元测试。 None 单元测试示例中的 Guzzle 对我来说很有意义如何在这种情况下实施,或者我只是看错了。

设置:Laravel 4.2 REST API - 控制器方法在方法中使用 Guzzle 从另一个 api 请求数据,如下所示:

<?php

class Widgets extends Controller {
    public function index(){
        // Stuff

        $client = new GuzzleHttp\Client();
        $url = "api.example.com";

        $response = $client->request('POST', $url, ['body' => array(...)]);

        // More stuff
    }
}

?>

我认为我可以按如下方式进行单元测试,并且一切正常。

function testGetAllWidgets(){
    $mock_response = array('foo' => 'bar');

    $mock = new MockHandler([
        new Response(200, $mock_response),
    ]);

    $handler = HandlerStack::create($mock);
    $client = new Client(['handler' => $handler]);

    $response = $this->call('GET', '/widgets');

    // Do asserts, etc.
}

但是,Guzzle 仍在向外部服务发出实际的 HTTP 请求。我的猜测可能是在 Controller 方法中设置客户端创建以使用 $handler,但我无法想象这是正确的方法。我错过了什么?

编辑 我的解决方案最终如下:

这个解决方案感觉最正确,也是Laravel方式。 (See IoC Containers)

我会在每个 api 调用上方添加此内容(根据 api 调用中需要模拟的外部调用数量更改模拟响应)。

$this->app->bind('MyController', function($app){
    $response_200 = json_encode(array("status" => "successful"));
    $response_300 = json_encode("MULTIPLE_CHOICES");

    $mock = new MockHandler([
        new Response(200, [], $response_200),
        new Response(300, [], $response_300)
    ]);

    $handler = HandlerStack::create($mock);

    return new MyController(new Client(['handler' => $handler]));
});

$params = array();

$response = $this->call('PUT', '/my-route', $params);

如果控制器需要 Guzzle 客户端,我将其添加到控制器中:

public function __construct(GuzzleHttp\Client $client)
{
    $this->client = $client;
}

然后将使用 $this->client 进行所有 api 调用。

使用https://github.com/php-vcr/php-vcr包。它有助于记录和重放 HTTP 请求。通过 Guzzle

测试 api 调用非常方便

"classic TDD" 对此的回应是您根本不应该对 Guzzle 进行单元测试。 Guzzle 是一个第三方库,应该(并且正在)由其自己的开发人员进行充分的完美测试。

您需要测试的是您的代码是否正确调用了 Guzzle,而不是 Guzzle 在您的代码调用时是否正常工作。

实现方式如下:

与其在控制器中执行 new Guzzle(),不如使用依赖注入将 Guzzle 对象传递到控制器中。幸运的是,Laravel 使这变得非常容易;您需要做的就是为您的控制器 class 提供一个构造函数方法,并将一个 Guzzle 对象定义为其参数之一。 Laravel 将完成创建对象并将其传递给您的其余部分。然后您的构造函数可以将其复制到 class 属性 以便您的其他方法可以使用它。

您的 class 现在应该看起来像这样:

class Widgets extends Controller {
    private $guzzle;
    public function __construct(GuzzleHttp\Client $guzzle)
    {
        $this->guzzle = $guzzle;
    }

    public function index(){
        // Stuff

        $url = "api.example.com";

        $response = $this->guzzle->request('POST', $url, ['body' => array(...)]);

        // More stuff
    }
}

现在你的测试应该更容易编写了。您可以在测试时将模拟 Guzzle 对象传递到您的 class 中。

现在您只需观察您的模拟 class 以确保对它的调用与 Guzzle API 期望接收的内容相匹配,以便进行调用。

如果您的 class 的其余部分取决于从 Guzzle 收到的输出,那么您也可以在模拟中定义它。

如果有人正在为此苦苦挣扎,那么我找到了替代品:

$this->app->bind('MyController', function($app){

$this->app->bind(MyController::class, function($app){

在 Laravel 5.5.44

对我有用吗