Reputation: 3790
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
Upvotes: 5
Views: 1379
Reputation: 581
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.
Upvotes: 1
Reputation: 1537
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());
}
}
Upvotes: 4