In my cases, I want to refactor the codebase but I don’t want to breaking change any request and response, previously I wrote feature test case in phpunit, but here are several problems:

  1. It’s a mock data for testing flow, it’s simular but not real.
  2. I need to prepare and mock many situations to assert it.
  3. I don’t want to bet and take a risk for refactoring.

So I make a new functions: Comparsion, it’s simple but useful. Here is my ideals.

  1. Simpler and faster for developers.
  2. provides make command and keep expected, actual endpoint
  3. I can specify any endpoint and environment.
  4. keep result into file.
  5. estimate execution time.
  6. tell developers what’s difference between expected and actual.

make:comparsion

It’s simple and extend from GeneratorCommand, preparing stub.

<?php

namespace App\Comparison\Console\Commands;

use Illuminate\Console\GeneratorCommand;

class ComparisonMakeCommand extends GeneratorCommand
{
    /**
     * The console command name.
     *
     * @var string
     */
    protected $name = 'make:comparison';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Create a new comparison';

    /**
     * The type of class being generated.
     *
     * @var string
     */
    protected $type = 'Tooling';

    /**
     * Get the stub file for the generator.
     *
     * @return string
     */
    protected function getStub()
    {
        return __DIR__ . '/stubs/comparison.stub';
    }

    /**
     * Get the default namespace for the class.
     *
     * @param  string  $rootNamespace
     * @return string
     */
    protected function getDefaultNamespace($rootNamespace)
    {
        return $rootNamespace.'\Comparison';
    }
}

app/Comparison/Console/Commands/stubs/comparison.stub As you can see, we can specify endpoint and customize header.

<?php

namespace DummyNamespace;

use App\Comparison\Illuminate\Comparison;
use App\User;

class DummyClass extends Comparison
{
    // endpoints = [openstack, dev, release, production]
    public function expected()
    {
        return $this->baseUrl('openstack')->authorize(User::find(1))->request('get', 'api/expected');
    }

    public function actual()
    {
        return $this->baseUrl('openstack')->authorize(User::find(1))->request('get', 'api/actual');
    }
}

Implementation Comparison

<?php

namespace App\Comparison\Illuminate;

use App\Services\UserService;
use App\User;
use GuzzleHttp\Client;
use Illuminate\Support\Arr;

abstract class Comparison
{
    protected $endpoints = [
        'openstack' => 'http://openstack.abc.com/api',
        'dev' => 'http://dev.abc.com/api',
        'release' => 'http://release.abc.com/api',
        'production' => 'http://abc.com/api',
    ];

    protected $baseUrl = null;

    /**
     * @var mixed
     */
    protected $accessToken = null;

    public function baseUrl(string $url = null)
    {
        if (isset($this->endpoints[$url])) {
            $this->baseUrl = $this->endpoints[$url];

            return $this;
        }

        $this->baseUrl = $url;

        return $this;
    }

    public function config(array $options = []): array
    {
        return [
            'base_uri' => $this->baseUrl,
            'headers' => [
                'Content-Type' => 'application/json',
                'Accept' => 'application/json',
            ],
        ] + $options;
    }

    /**
     * endpoint.
     */
    public function request($method, $uri = '', array $options = [])
    {
        $config = $this->config($options);
        if ($this->accessToken) {
            Arr::set($config, 'headers.Authorization', 'Bearer '.$this->accessToken);
        }

        $response = (new Client($config))->request($method, $uri, $options);

        return json_decode($response->getBody()->getContents(), true);
    }

    public function authorize(User $user)
    {
        $this->accessToken = app(UserService::class)->generateToken($user);

        return $this;
    }

Compare:with

Next, I need to create compare:with command:

<?php

namespace App\Comparison\Console\Commands;

use App\Exceptions\ClassNotFoundException;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
use PHPUnit\Framework\ExpectationFailedException;

class ComparingWith extends Command
{
    protected $signature = 'compare:with {comparison} {--f|force}';

    protected $description = 'Compares api1 to api2 response are the same payload.';

    public function __construct()
    {
        parent::__construct();
    }

    public function handle()
    {
        // if request once, no force retrieving, I will get data from cache file to reducing request cost.
        $force = $this->option('force');

        $class = '\\App\\Comparison\\'.$this->argument('comparison');
        throw_unless(class_exists($class), new ClassNotFoundException('The '.$class. ' does not exist.'));

        $filename = now()->format('Ymd').'_'.$this->argument('comparison').'.json';

        $instance = app($class);
        $expectedNow = now();
        $expectedExecutedTime = 'cache_file';
        $expectedPath = 'comparison'.DIRECTORY_SEPARATOR.$filename;
        if (! Storage::disk('local')->exists($expectedPath) OR $force == true) {
            Storage::disk('local')->put($expectedPath, json_encode($instance->expected()));
            $expectedExecutedTime = $expectedNow->diffInRealMicroseconds(now()) / pow(10, 6);
        }

        $expected = Storage::disk('local')->get($expectedPath);
        $expectedDecode = json_decode($expected, true);

        $actualNow = now();
        $actual = $instance->actual();
        $actualExecutedTime = $actualNow->diffInRealMicroseconds(now()) / pow(10, 6);

        // in real requesting.
        if ($expectedExecutedTime != 'cache_file') {
            $boosting = $expectedExecutedTime - $actualExecutedTime;
            $this->info("Expected: $expectedExecutedTime s, Actual: $actualExecutedTime s, Boosting: $boosting s.");
        }

        try {
            // See in below implementation.
            (new Assert)->assertEquals($expectedDecode, $actual);
        } catch(ExpectationFailedException $e) {
            $this->alert($e->getComparisonFailure()->getDiff());
            return 1;
        }

        $this->info("Congratulations 🎉 The APIs are the same result.");
        return 0;
    }
}

Creating Excpetion and register command in Kernel:

// ClassNotFoundException.php
<?php

namespace App\Exceptions;

use Exception;

class ClassNotFoundException extends Exception {}

//Kernel
protected $commands = [
        //
        \App\Comparison\Console\Commands\ComparingWith::class,
        \App\Comparison\Console\Commands\ComparisonMakeCommand::class,
    ];

When I execute compare:with I want to make where’s difference between expected and actual prompt information, so I make a trick:

//app/Comparison/Console/Commands/Assert.php
<?php

namespace App\Comparison\Console\Commands;

use Tests\TestCase;

/**
 * This class is for using assert handler.
 */
class Assert extends TestCase {}

It’s a simple implementation and improvale, I will update it if it has a better thinking, cheers.