4

While testing:

While checkout items from my website, need to mock confirmation... so we can then continue processing the order. Where the testing can be done..

How would i swap out good code for a mock? such as:

$gateway = Omnipay::create('paypal');
$response = $gateway->purchase($request['params'])->send();
if ($response->isSuccessful()) { ... etc ...

How is this possible?

While i have created tests, my knowledge in the area of mocking is basic

enter image description here

Harry Bosh
  • 3,611
  • 2
  • 36
  • 34
  • https://stackoverflow.com/questions/44264952/passing-a-mocked-object-as-the-instance-in-phpunit-laravel – Harry Bosh Feb 21 '18 at 22:46
  • Please show how you got mocking to work with laravel and omnipay/paypal. So people can test checkouts without going to paypal – Harry Bosh Feb 28 '20 at 01:56
  • What is the `$checkout` variable? Add more context surrounding that in your code snippet. – Oluwafemi Sule Feb 29 '20 at 00:32
  • I think we need a longer snippet than those two lines... I'm not familiar with Paypal's PHP SDK, but I'm very familiar with PHPUnit. If you show a bit more of what your payment process looks like then it's going to be easier to help. – mrodo Mar 01 '20 at 00:50
  • @HarryBosh have you seen my answer? – mrodo Mar 04 '20 at 10:07

2 Answers2

3

As far as it depends t mocking, you don't need to know exact response, you just need to know inputs and outputs data and you should replace your service (Paypal in this case) in laravel service provider. You need some steps like bellow: First add a PaymentProvider to your laravel service provider:

class AppServiceProvider extends ServiceProvider
{
   ...

   /**
    * Register any application services.
    *
    * @return void
    */
    public function register()
    {
        $this->app->bind(PaymentProviderInterface::class, function ($app) {
            $httpClient = $this->app()->make(Guzzle::class);
            return new PaypalPackageYourAreUsing($requiredDataForYourPackage, $httpClient);
        });
    }

    ...
}

Then in your test class you should replace your provider with a mock version of that interface:

class PaypalPackageTest extends TestCase
{
   /** @test */
   public function it_should_call_to_paypal_endpoint()
   {
       $requiredData = $this->faker->url;
       $httpClient = $this->createMock(Guzzle::class);
       $paypalClient = $this->getMockBuilder(PaymentProviderInterface::class)
           ->setConstructorArgs([$requiredData, $httpClient])
           ->setMethod(['call'])
           ->getMock();

       $this->instance(PaymentProviderInterface::class, $paypalClient);

       $paypalClient->expects($this->once())->method('call')->with($requiredData)
           ->willReturn($httpClient);

       $this->assertInstanceOf($httpClient, $paypalClient->pay());
   }
}
train_fox
  • 1,517
  • 1
  • 12
  • 31
0

This is the approach I usually take when I have to mock methods that contain calls to external libraries (such as Omnipay in your case).

Your snippet isn't very extensive, but I'll assume your class looks something like this:

class PaymentProvider
{
    public function pay($request)
    {
        $gateway = Omnipay::create('paypal');
        $response = $gateway->purchase($request['params'])->send();
        if ($response->isSuccessful()) {
                // do more stuff
        }
    }
}

What I would do is refactor the class, so that the call to the external library is inside a separate method:

class PaymentProvider
{
    protected function purchaseThroughOmnipay($params)
    {
        $gateway = Omnipay::create('paypal');
        return $gateway->purchase($params)->send();
    }

    public function pay($request)
    {
        $response = $this->purchaseThroughOmnipay($request['params']);
        if ($response->isSuccessful()) {
                // do more stuff
        }
    }
}

Then, after this refactoring, in the test class we can take advantage of the many possibilities PHPunit's getMockBuilder gives us:

<?php

use PHPUnit\Framework\TestCase;

class PaymentProviderTest extends TestCase
{
    protected $paymentProvider;

    protected function setUp()
    {
        $this->paymentProvider = $this->getMockBuilder(\PaymentProvider::class)
                ->setMethods(['pay'])
                ->getMock();
    }

    public function testPay()
    {
        // here we set up all the conditions for our test
        $omnipayResponse = $this->getMockBuilder(<fully qualified name of the Omnipay response class>::class)
                ->getMock();

        $omnipayResponse->expects($this->once())
                ->method('isSuccessful')
                ->willReturn(true);

        $this->paymentProvider->expects($this->once())
                ->method('purchaseThroughOmnipay')
                ->willReturn($omnipayResponse);

        $request = [
            // add relevant data here
        ];

        // call to execute the method you want to actually test
        $result = $this->paymentProvider->pay($request);

        // do assertions here on $result
    }
}

Some explanation of what's happening:

$this->paymentProvider = $this->getMockBuilder(\PaymentProvider::class)
    ->setMethods(['pay'])
    ->getMock();

This gives us a mock instance of the Payment class, for which pay is a "real" method whose actual code is actually executed, and all other methods (in our case, purchaseThroughOmnipay is the one we care about) are stubs for which we can override the return value.

In the same way, here we are mocking the response class, so that we can then control its behavoir and influence the flow of the pay method:

$omnipayResponse = $this->getMockBuilder(<fully qualified name of the Omnipay response class>::class)
    ->getMock();

$omnipayResponse->expects($this->once())
    ->method('isSuccessful')
    ->willReturn(true);

The difference here is that we are not calling setMethods, which means that all the methods of this class will be stubs for which we can override the return value (which is exactly what we are doing for isSuccessful). Of course, in case more methods of this class are called in the pay method (presumably after the if), then you will probably have to use expect more than once.

mrodo
  • 565
  • 5
  • 21