Easily Create Fixture Data from Remote Services and Refresh Mock Data

Posted: 2015-05-27 01:03:12

Oops Codeship and Laravel is here

Easily Create Fixture Data from Remote Services and Refresh Mock Data

We have integration tests that hit remote apis like Github, S3, DynomoDB, our own APIs etc and we do not want to hit those during out tests but we also want to make sure we have the real data. So when one of those APIs change then our mock data can be refreshed to see if our systems really work with it.

Using Laravel's new integration tests, though this works with any framework, we will swap out these Service classes with a Wrapper class ONLY if we have a matching file. This allows us to delete those files and get another one on the fly.

Lets start with the Controller

This simple Controller will talk to a Repo. Imagine the repo talking to Dynamodb or GithubApi, database etc.

<?php

namespace App\Http\Controllers;


use App\ExampleRepo;
use Illuminate\Support\Facades\Response;

class ExampleController extends Controller
{

    public function mocking(ExampleRepo $exampleRepo)
    {
        return Response::json("You are here " . $exampleRepo->get());
    }
}

So thanks to the dependency injection system; and the use of the Reflection Class, ExampleRepo get constructed as well.

The Tests

First lets look at a normal test no mock

<?php

use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseTransactions;

use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\File;
use Mockery as m;

class ExampleTest extends TestCase
{
    public function testDefault()
    {
        $this->get('/mocking')->see("You are here foo");
    }
}

Pretty simple. But not lets...

Swap Things Out

Here we add an example of replacing the default instance App would make with our own Wrapper

<?php

use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseTransactions;

use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\File;
use Mockery as m;

class ExampleTest extends TestCase
{
    public function testDefault()
    {
        $this->get('/mocking')->see("You are here foo");
    }

    public function testMocking()
    {
        $mock = m::mock('App\ExampleRepo');
        $mock->shouldReceive('get')->once()->andReturn('bar');
        App::instance('App\ExampleRepo', $mock);

        $this->get('/mocking')->see("You are here bar");
    }

}

testMocking will now return bar!

Making Fixtures on the Fly

Same results BUT we hit my wrapper not the real services. But here is where I think it gets even better. I can return fixture data BUT at the same time I can not worry about returning stale fixture data eg the apis have changed but my fixtures have not. All of this without having my test code wrapped into the app code.

This will look for the output of a route. That Controller and Repo we will show in a moment

    public function testMakeFixture()
    {
        $wrapper = App::make('App\ExampleRepoWrapper');
        App::instance('App\ExampleRepo', $wrapper);

        $this->get('/mocking')->see("You are here foo");
    }

This test has a wrapper which extends the repo

<?php

namespace App;


use Illuminate\Support\Facades\File;

class ExampleRepoWrapper extends ExampleRepo
{
    public function get()
    {

        if(File::exists(base_path('tests/fixtures/foo.json')))
        {
            $content = File::get(base_path('tests/fixtures/foo.json'));
            return json_decode($content, true);
        }

        $results = parent::get();

        if(!File::exists(base_path('tests/fixtures/foo.json')))
        {
            $content = json_encode($results, JSON_PRETTY_PRINT);
            File::put(base_path('tests/fixtures/foo.json'), $content);
        }

        return $results;
    }
}

So now the Controller will talk to the Wrapper instead which will look for a file (NOTE: You can easily pass in $id or $name to make the fixtures unique)

So now when the Controller hits our Wrapper it goes right to the real ExampleRepo (seen below) if there is no fixture file and then the Wrapper kicks in to make the file (as seen in the above class).

<?php namespace App;


class ExampleRepo
{

    protected $results;

    public function get()
    {
        $this->results = 'foo';
        return $this->results;
    }

    /**
     * @return mixed
     */
    public function getResults()
    {
        return $this->results;
    }

}

That is it you can do integration testing on your APIs and not hit external services or even databases.

Force Full Integration

Sometimes you want to hit the external resources. This can be part of a weekly or daily test to make sure you app is working with all the external APIs. You can do this by deleting all the fixtures before running that test.

So you can setup a provider like this


class ExampleProvider extends ServiceProvider { public function register() { if(App::environment() == 'testing' and env('FULL_INTEGRATION') != 'true') { $this->app->bind('App\ExampleRepo', 'App\ExampleRepoWrapper'); } else { $this->app->bind('App\ExampleRepo', 'App\ExampleRepo'); } } } ** UPDATE ** Another good idea, by [Nathan Kirschbaum](https://twitter.com/n_kirschbaum), is to set the `FULL_INTEGRATION` setting by the user that is logged in.

Cons

One is UI testing. Prior to this I would make wrappers as needed to then take over if say APP_MOCK=true. Then I could mock even on Behat testing or the UI. But that meant a lot of Providers and alot of mixing of testing and code. But it worked and ran well on services like CodeShip and locally. If you Behat/Acceptance tests are hitting the API or UI it would be nice to fake all the external responses. Though now with the above the API testing is easy. The UI (when there is javascript) not so easy :(

Since we are using App::instance we did not need to register a Provider Class. But to make the UI con a non issue you can go that far to register a ServiceProvider


class ExampleProvider extends ServiceProvider { public function register() { if(App::environment() == 'testing' { $this->app->bind('App\ExampleRepo', 'App\ExampleRepoWrapper'); } else { $this->app->bind('App\ExampleRepo', 'App\ExampleRepo'); } } }

Then register as normal in your config/app.php . This can be kinda tedious but would produce the same results.

Great book on the topic Laravel Testing Decoded

decoded