Most of the time when I'm writing test code in Laravel I take
advantage of the great Storage::fake()
provided by Laravel
Test Suite. However, I usually like to have at least 1 test
that feels a bit less "mocky". That's where Minio comes in.
Minio is a storage service fully compatible with S3. Here's
how simple it is for me to write test code with it.
Docker
I use Minio's docker image to run the service in a container linked with my source code through docker-compose. The service declaration looks like this:
minio:
image: minio/minio
environment:
- MINIO_ACCESS_KEY=customergauge
- MINIO_SECRET_KEY=phpunit123
command: server /data
As long as I have a php container with my Laravel code in the
same docker-compose file, I'm able to communicate with minio
using http://minio:9000
.
Test Code
My Test Code will look like this:
$minio = new Minio();
$minio->disk('my-bucket', function (S3Client $client, string $bucket) {
$this->post('/my/endpoint/that/interacts/with/s3', [])
->assertSuccessful();
$object = $client->getObject([
'Bucket' => $bucket,
'Key' => "/my/expected/s3/key"
]);
$content = $object['Body']->getContents();
$this->assertStringContainsString('partial-file-content', $content);
});
When instantiating the Minio class, I have the opportunity
to overwrite some defaults. For instance, if for some reason
the container name is not minio
, I can specify a different
host:
$minio = new Minio('my-host', 9000, 'access-key', 'secret');
Minio's disk
method will take a bucket name and a callback.
In the callback we're free to do whatever we need to
test our code against S3. It usually involves calling
a Laravel endpoint and making assertions against the file
that was uploaded to S3. The S3Client provided to the callback
will already be configured to communicate with Minio.
The Minio Class
For convenience, I opened source the minio class at
https://github.com/cgauge/laravel-s3-minio and you call grab
it by running composer require customergauge/minio
, but if
you don't want to rely on a package, the class is pretty simple
and straightforward:
<?php declare(strict_types=1);
namespace CustomerGauge\Minio\Testing;
use Aws\S3\S3Client;
use Illuminate\Contracts\Config\Repository;
use Throwable;
class Minio
{
private $host;
private $port;
private $key;
private $secret;
private $config;
public function __construct(
string $host = 'minio',
int $port = 9000,
string $key = 'customergauge',
string $secret = 'phpunit123',
?Repository $config = null
) {
$this->host = $host;
$this->port = $port;
$this->key = $key;
$this->secret = $secret;
$this->config = $config ?? config();
}
public function disk(string $disk, callable $callback)
{
$endpoint = 'http://' . $this->host . ':' . $this->port;
$client = new S3Client([
'region' => 'local',
'version' => '2006-03-01',
'endpoint' => $endpoint,
'use_path_style_endpoint' => true,
'credentials' => [
'key' => $this->key,
'secret' => $this->secret,
],
]);
$bucket = "$disk-bucket";
// Let's go ahead and configure Laravel filesystem with the provided disk
// so that the code being tested can properly interact with minio.
$this->config->set("filesystems.disks.$disk", [
'driver' => 's3',
'region' => 'local',
'bucket' => $bucket,
'endpoint' => $endpoint,
'use_path_style_endpoint' => true,
'key' => $this->key,
'secret' => $this->secret,
]);
try {
// If the bucket already exists, it will throw an exception which we can ignore.
// If the bucket doesn't exist yet, let's create it so that the code will be
// able to properly interact with it.
$client->createBucket(['Bucket' => $bucket]);
} catch (Throwable $e) {
}
try {
$callback($client, $bucket);
} finally {
// Whether the developer's code fails or not, we can make a best effort
// into cleaning up the bucket and deleting it so thatnext time the
// test runs, we can successfully create an empty bucket.
$iterator = $client->getIterator('ListObjects', ['Bucket' => $bucket]);
foreach ($iterator as $object) {
$client->deleteObject([
'Bucket' => $bucket,
'Key' => $object['Key']
]);
}
$client->deleteBucket(['Bucket' => $bucket]);
}
}
}
Conclusion
Most of the time, Storage::fake()
will do just fine, but
sometimes we want to make sure our code is compatible with
S3 or even just get an extra guarantee that our code's
interaction with AWS SDK is property tested. Minio
is a great way of testing the code's compatibility
with S3 without actually having to use an actual S3 bucket.
As always, hit me up on Twitter with any questions.
Cheers.