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:
- It’s a mock data for testing flow, it’s simular but not real.
- I need to prepare and mock many situations to assert it.
- 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.
- Simpler and faster for developers.
- provides make command and keep expected, actual endpoint
- I can specify any endpoint and environment.
- keep result into file.
- estimate execution time.
- 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.