在 Lumen 的路由控制器中模拟方法调用以进行测试

Mocking a method call in a route controller in Lumen for testing

我在实现路由控制器的 Lumen 应用程序中有以下控制器 class:

<?php

class MyController {
    public function route_method(Request $request) {
        // some code
        $success = $this->private_method($request->get('get_variable'));
        // some code
        return \response()->json(['results' => $success]);
    }

    private function private_method($data) {
        // some code, calling 3rd party service
        return $some_value;
    }
}

以及Lumen应用中的如下对应路由web.php

<?php

$app->get('/endpoint', ['uses' => 'MyController@route_method']);

现在我想编写单元测试来确认调用 /endpoint 的返回响应 returns 预期的 JSON 响应包含 key/value 对 'results': true,但不让 route_method() 通过模拟后者调用 private_method(),因为 - 如评论中所述 - private_method() 调用第三方服务,我想避免这种情况,所以我我想我需要这样的东西:

<?php

class RouteTest extends TestCase {
    public function testRouteReturnsExpectedJsonResponse() {
        // need to mock the private_method here somehow first, then...
        $this->json('GET', '/endpoint', ['get_variable' => 'get_value'])->seeJson(['results' => true]);
    }
}

但是我如何利用 Mockery 来达到这个目的,或者有其他方法来隔离第 3 方调用吗?

您无法模拟此代码这一事实表明代码设计不当。 我在这里展示的例子只是一个想法,重点是创建一个新的 class 代表与第 3 方系统的通信。

<?php

namespace App\Http\Controllers;

use App\Services\MyService;

class MyController
{
    public function __construct(MyService $service)
    {
        $this->service = $service;
    }

    public function route_method(Request $request)
    {
        // some code
        $success = $this->service->some_method($request->get('get_variable'));
        // some code
        return \response()->json(['results' => $success]);
    }
}

然后创建另一个 class 来完成它应该做的事情,遵循单一职责原则

<?

namespace App\Services;

class MyService
{
    public function some_method($variable)
    {
        //do something
    }
}

那你就可以正常mock了:

<?php

class RouteTest extends TestCase {
    public function testRouteReturnsExpectedJsonResponse() {

        $service = $this->getMockBuilder('App\Services\MyService')
            ->disableOriginalConstructor()
            ->getMock();

        $somedata = 'some_data' //the data that mock should return
        $service->expects($this->any())
            ->method('some_method')
            ->willReturn($somedata);

        //mock the service instance    
        $this->app->instance('App\Services\MyService', $service);

        // need to mock the private_method here somehow first, then...
        $this->json('GET', '/endpoint', ['get_variable' => 'get_value'])->seeJson(['results' => true]);
    }
}

基本上,你不会。

您应该测试行为,而不是实施。私有方法是一个实现细节。

尽管如此,您仍然可以随心所欲,Laravel/Lumen 中提供了许多选项:

正确的做法:

看看@Felippe Duarte 的回答。要使用 Mockery 而不是 PHPUnit 添加测试代码进行模拟:

<?php

class RouteTest extends TestCase
{
    public function testRouteReturnsExpectedJsonResponse()
    {
        $someData = 'some_data'; //the data that mock should return

        $service = Mockery::mock('App\Services\MyService');
        $service->shouldReceive('some_method')->once()->andReturn($someData);

        //mock the service instance
        $this->app->instance('App\Services\MyService', $service);

        // need to mock the private_method here somehow first, then...
        $this->json('GET', '/endpoint', ['get_variable' => 'get_value'])->seeJson(['results' => $someData]);
    }
}

服务容器的滥用方式:

控制器:

<?php

class MyController {
    public function route_method(Request $request) {
        // some code
        $success = $this->private_method($request->get('get_variable'));
        // some code
        return \response()->json(['results' => $success]);
    }

    private function private_method($data) {
        // say third party is some paypal class
        $thirdParty = app(Paypal::class);

        return $thirdParty->makePayment($data);
    }
}

测试:

<?php

class RouteTest extends TestCase
{
    public function testRouteReturnsExpectedJsonResponse()
    {
        $someData = 'some_data'; //the data that mock should return

        $paypalMock = Mockery::mock(Paypal::class);
        $paypalMock->shouldReceive('makePayment')->once()->with('get_value')->andReturn($someData);

        //mock the service instance
        $this->app->instance(Paypal::class, $paypalMock);

        // need to mock the private_method here somehow first, then...
        $this->json('GET', '/endpoint', ['get_variable' => 'get_value'])->seeJson(['results' => $someData]);
    }
}

这会起作用,因为 Laravel 的 Service Container 会识别您定义的当尝试实例化 Paypal::class 时,它应该 return 测试中制作的模拟.

不推荐这样做,因为它很容易被滥用,而且不是很明确。