1

I have the following controller class in my Lumen application that implements the route controller:

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

And the following corresponding route in Lumen application web.php:

<?php

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

Now I want to write the unit test that confirms the returned response for call /endpoint returns an expected JSON response that contains the key/value pair of 'results': true, but without letting route_method() call private_method() by mocking the latter, because - as in the comment - the private_method() calls a 3rd party service and I want to avoid that, so I think I need something like this:

<?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]);
    }
}

But how do I make use of Mockery for this purpose, or is there another way to isolate the 3rd party call?

MMSs
  • 455
  • 6
  • 19

2 Answers2

3

The fact you can't mock this code is a sign for bad code design. The example I'm showing here is just an idea, the point is to create a new class that you represent the communication with the 3rd party system.

<?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]);
    }
}

Then create another class that will do what it should do, following the Single Responsability Principle

<?

namespace App\Services;

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

Then you can mock properly:

<?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]);
    }
}
Felippe Duarte
  • 14,901
  • 2
  • 25
  • 29
  • Thanks! In fact `MyService` class does exist, but I thought I could mock the `private_method` in the same class, not `MyService` class. But I understand from you `private_method` cannot be mocked here, is that correct? – MMSs Apr 05 '18 at 15:54
  • I think the point here is about responsability. The controller has no responsability to call a 3rd party. But, you can get more info here: https://stackoverflow.com/questions/5937845/mock-private-method-with-phpunit – Felippe Duarte Apr 05 '18 at 15:59
3

Basically, you don't.

You should be testing behavior, not implementation. A private method is an implementation detail.

Still, you're free to do whatever you want, and many options are available in Laravel/Lumen :

The proper way :

Look at @Felippe Duarte answer. To add the testing code using Mockery instead of PHPUnit for mocking :

<?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]);
    }
}

The Service Container abusive way :

Controller :

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

Test :

<?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]);
    }
}

This will work, because the Service Container of Laravel will recognize you defined that when trying to instantiate Paypal::class, it should return the mock made in the test.

This is not recommended because it's easy to abuse it, and it's not very explicit.

Steve Chamaillard
  • 2,289
  • 17
  • 32
  • Ok, I changed the way I'm making the test to not be mocking the private method, but there's still something unclear. It seems from your code that `$this->app->instance()` call sets the mock object to respond `some_method` call, but that is still not the case for me. The mocked object isn't the one handling the `some_method` call when `$this-json()` call is triggered. – MMSs Apr 09 '18 at 09:01