SmoDav/mpesa

Testing mpesa by using Guzzle's MockHandler is not working out

Naoray opened this issue · 3 comments

Thanks for the package!

I use Laravel v6.0.4 and v4.1.0 of this package.

In general this package seems to be working fine. However, I went into an issue when trying to test our own implementation of this package by using Guzzle's Mock Handler.

$this->app->bind(Core::class, function ($app) {
            $config = $app->make(ConfigurationStore::class);
            $cache = $app->make(CacheStore::class);

            $mock = new MockHandler([
                new Response(200, [], json_encode([
                    'ResponseCode' => '0',
                    'expires_in' => 3600,
                    'access_token' => 'fake-token',
                ])),
                new Response(200, [], json_encode($fakeBodyResponse)),
            ]);
            $handler = new HandlerStack($mock);

            $client = new Client(['handler' => $handler]);

            return new Core($client, $config, $cache);
        });

When executing my test seperated it works fine, but when I run the whole testsuite it does not work. The reason seems to be the instance() method on the Core class (s. https://github.com/SmoDav/mpesa/blob/master/src/Mpesa/Engine/Core.php#L76-L87). Since the Core class is only resolved out of the app container once the binding from this packages (ServiceProvider)[https://github.com/SmoDav/mpesa/blob/master/src/Mpesa/Laravel/ServiceProvider.php#L43-L48] is used and my overriden binding is ignored.

Solution

I replaced the binding in your package with a singleton() and also resolve the Client out of the container by using Guzzle's ClientInterface. That way I wouldn't even need to override the binding of Core in my tests.

$this->app->singleton(Core::class, function ($app) {
            $config = $app->make(ConfigurationStore::class);
            $cache = $app->make(CacheStore::class);

            return new Core(resolve(ClientInterface::class), $config, $cache);
        });

But because of the instance() method in Core being used all over the place it only gets resolved once and can't get overriden or even accept a different Client out of the container. So, I basically replaced all Core::instance() calls with resolve(Core::class) and everything worked as supposed to.

Problem

I am aware that this package is not only for Laravel usage, but using the instance() method in a Laravel context doesn't make a lot of sense since you are basically trying to implement the singleton functionality which is already present in Laravel.


Also I am not very familiar with other frameworks, but as far as I am aware they do not tend to have an app() method which you call at https://github.com/SmoDav/mpesa/blob/master/src/Mpesa/Engine/Core.php#L83 anyways no matter which framework.

That being said, IMO if you want to maintain a vanilla PHP usable version and one for Laravel you should split those into two separate packages.

Hope that makes sense?!

Thanks for taking the time to read through this :).

Hey Naoray, I've got a v5 update for the package, which I should be committing soon, maybe in an hour or so. Anyway, you can test out the implementation using Mocker and binding it to the application. Here's an example of the STK push and validation.

<?php

namespace Tests\Feature;

use GuzzleHttp\Client;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
use SmoDav\Mpesa\Engine\Core;
use SmoDav\Mpesa\Laravel\Facades\STK;
use SmoDav\Mpesa\Laravel\Stores\LaravelCache;
use SmoDav\Mpesa\Laravel\Stores\LaravelConfig;
use Tests\TestCase;

class ExampleMPesaTest extends TestCase
{
    /*
     * Test that authenticator works.
     *
     * @test
     **/
    public function testPushRequest()
    {
        $response = [
            'MerchantRequestID' => '19465-780693-1',
            'CheckoutRequestID' => 'ws_CO_27072017154747416',
            'ResponseCode' => 0,
            'ResponseDescription' => 'Success. Request accepted for processing',
            'CustomerMessage' => 'Success. Request accepted for processing',
        ];

        $mock = new MockHandler([
            new Response(200, [], json_encode(['access_token' => 'access', 'expires_in' => 3599])),
            new Response(200, [], json_encode($response)),
        ]);

        $core = new Core(
            new Client(['handler' => HandlerStack::create($mock)]),
            $this->app->make(LaravelConfig::class),
            $this->app->make(LaravelCache::class),
        );

        $this->instance(Core::class, $core);

        $response = STK::push(100, 254722000000, 'Test', 'Awesome');

        $this->assertEquals('19465-780693-1', $response->MerchantRequestID);
        $this->assertEquals('ws_CO_27072017154747416', $response->CheckoutRequestID);
        $this->assertEquals(0, $response->ResponseCode);
        $this->assertEquals('Success. Request accepted for processing', $response->ResponseDescription);
        $this->assertEquals('Success. Request accepted for processing', $response->CustomerMessage);
    }

    public function testValidateTransaction()
    {
        $response = [
            'MerchantRequestID' => '19465-780693-1',
            'CheckoutRequestID' => 'ws_CO_27072017154747416',
            'ResponseCode' => 0,
            'ResponseDescription' => 'Success. Request accepted for processing',
            'ResultCode' => 0,
            'ResultDesc' => 'The service request is processed successfully.'
        ];

        $mock = new MockHandler([
            new Response(200, [], json_encode(['access_token' => 'access', 'expires_in' => 3599])),
            new Response(200, [], json_encode($response)),
        ]);

        $core = new Core(
            new Client(['handler' => HandlerStack::create($mock)]),
            $this->app->make(LaravelConfig::class),
            $this->app->make(LaravelCache::class),
        );

        $this->instance(Core::class, $core);
        $response = STK::validate('ws_CO_27072017154747416');

        $this->assertEquals('19465-780693-1', $response->MerchantRequestID);
        $this->assertEquals('ws_CO_27072017154747416', $response->CheckoutRequestID);
        $this->assertEquals(0, $response->ResponseCode);
        $this->assertEquals('Success. Request accepted for processing', $response->ResponseDescription);
        $this->assertEquals(0, $response->ResultCode);
        $this->assertEquals('The service request is processed successfully.', $response->ResultDesc);
    }
}

Hey @SmoDav , thanks for the fast reply.

A the instance(Core::class, $core) did the trick, thank you!

Your v5.0 version looks much cleaner! One thing I'd change is how the callback url is identified. Maybe just add a method to set it dynamically instead of relying on the config for this...