Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Minio Testing Tools Laravel Package

protonemedia/laravel-minio-testing-tools

Testing helpers for Laravel that make it easy to use MinIO, the S3-compatible object storage server, in your test suite. Spin up buckets, configure filesystem disks, and assert file uploads without touching real AWS S3, keeping tests fast, isolated, and repeatable.

View on GitHub
Deep Wiki
Context7

Getting Started

This package exists to solve one specific problem: Storage::fake('s3') is a local filesystem wearing an S3 costume. It never signs a request, never issues a presigned URL, never touches multipart uploads, and never validates path-style vs. virtual-host addressing. If your app depends on any of that, you need a real S3 API in your test suite. MinIO is a single-binary, S3-compatible server — this package's job is to start it, point your s3 disk at it, and tear it down.

Install and get a MinIO binary available:

composer require protonemedia/laravel-minio-testing-tools --dev

# Get the server binary: either the package's download command
# (see the README for the exact artisan signature in your version)
php artisan minio:download

# ...or install it yourself, which is often more predictable in CI
brew install minio/stable/minio          # macOS
# or: curl -O https://dl.min.io/server/minio/release/linux-amd64/minio && chmod +x minio

Wire the trait into your base test case:

// tests/TestCase.php
use ProtoneMedia\LaravelMinioTestingTools\InteractsWithMinio;

abstract class TestCase extends BaseTestCase
{
    use InteractsWithMinio;
}

Point the s3 disk at the local server. Put this in phpunit.xml so the whole suite sees it, not in .env:

<env name="AWS_ACCESS_KEY_ID" value="minioadmin"/>
<env name="AWS_SECRET_ACCESS_KEY" value="minioadmin"/>
<env name="AWS_DEFAULT_REGION" value="us-east-1"/>
<env name="AWS_BUCKET" value="testing"/>
<env name="AWS_ENDPOINT" value="http://127.0.0.1:9000"/>
<env name="AWS_USE_PATH_STYLE_ENDPOINT" value="true"/>

First use case — the one that justifies the dependency. Write a test that proves your presigned-URL flow actually works end to end. This is the test Storage::fake() structurally cannot give you:

it('serves a private file through a signed url', function () {
    Storage::disk('s3')->put('private/report.pdf', 'secret contents');

    $url = Storage::disk('s3')->temporaryUrl('private/report.pdf', now()->addMinutes(5));

    Http::get($url)->assertOk()->assertSee('secret contents');

    // and that an unsigned URL is rejected
    $unsigned = strtok($url, '?');
    Http::get($unsigned)->assertForbidden();
});

If that test passes, the trait is doing its job.

Implementation Patterns

Trait placement is a cost decision. Putting InteractsWithMinio on your base TestCase means every test in the suite pays for a MinIO boot, a bucket creation, and — worse — inherits leftover objects from previous tests. Prefer a dedicated base class or per-file usage, and keep the plain Storage::fake('s3') path for tests that only care that something was written:

// tests/Feature/S3TestCase.php
abstract class S3TestCase extends TestCase
{
    use InteractsWithMinio;
}

// Pest
uses(S3TestCase::class)->in('Feature/Storage');

Bucket hygiene between tests. MinIO persists across tests within a run, so bucket contents are your new global state. Treat the disk like a database and truncate it in setUp():

protected function setUp(): void
{
    parent::setUp();

    Storage::disk('s3')->deleteDirectory('/');
}

If the trait boots MinIO once per process rather than once per test, this is the only thing keeping tests independent. Watch for a test that passes alone and fails in the suite — that's almost always a leaked object.

Resolve the disk after the trait sets config. Laravel caches resolved disk instances on the Storage manager. If anything resolves Storage::disk('s3') before the trait swaps the endpoint in, you'll hit real AWS (or fail) with a confusing error. Defend with Storage::forgetDisk('s3') in setUp() if you see this.

Test the S3-specific behaviours, not the generic ones. The high-value targets:

// visibility / ACLs — differs meaningfully between local and S3
Storage::disk('s3')->setVisibility('public/logo.png', 'public');
expect(Storage::disk('s3')->getVisibility('public/logo.png'))->toBe('public');

// metadata round-trip
Storage::disk('s3')->put('a.txt', 'x', ['Metadata' => ['owner' => 'u-1']]);

// multipart thresholds — force a small chunk size to exercise the code path cheaply
// (configure the disk with a low multipart threshold in the test itself)

// controller that streams a download from S3
$this->get('/downloads/1')->assertDownload();

Direct-to-S3 upload tests (the big one). If you use Vapor, Livewire's WithFileUploads S3 flow, or any "client PUTs to a presigned URL" pattern, this package lets you simulate the browser step faithfully:

$url = Storage::disk('s3')->temporaryUploadUrl('uploads/1.zip', now()->addMinutes(5));

$this->call('PUT', $url['url'], [], [], [], $url['headers'], 'payload');

Storage::disk('s3')->assertExists('uploads/1.zip');

Keep it out of the fast lane. Structure the suite so MinIO tests are a tag/group you can run alone in a pre-merge job:

phpunit --exclude-group minio      # fast, mostly Storage::fake()
phpunit --group minio             # slow, faithful

Parallelism requires port isolation. With phpunit --parallel or Pest's parallel runner, N processes will fight over port 9000 and the same data directory. Give each TEST_TOKEN its own port (and, if the package exposes it, its own storage dir) via config in setUp():

config(['minio-testing-tools.port' => 9000 + (int) env('TEST_TOKEN', 0)]);

CI. Service containers are more reliable than shelling out to a binary:

services:
  minio:
    image: minio/minio
    env:
      MINIO_ROOT_USER: minioadmin
      MINIO_ROOT_PASSWORD: minioadmin
    ports: ['9000:9000']
    options: >-
      --health-cmd "curl -f http://localhost:9000/minio/health/live"

Then let the trait detect an already-running server, or skip its boot step. Verify what your version supports before relying on this — it's the single most likely place this package stops earning its keep.

Gotchas and Tips

  • AWS_USE_PATH_STYLE_ENDPOINT=true is non-negotiable. bucket.localhost:9000 does not resolve. Omitting this produces Could not resolve host or a 404 from MinIO, which reads like a credentials bug and isn't.
  • Never call Storage::fake('s3') in a MinIO test. It silently replaces the disk with the local adapter and your presigned-URL test passes for the wrong reason. If you're debugging "why isn't this hitting MinIO," grep for fake(.
  • Region matters for signing. An empty or wrong AWS_DEFAULT_REGION gets you AuthorizationHeaderMalformed or a signature mismatch. us-east-1 is the safe default; make it explicit in phpunit.xml.
  • Empty credentials break MinIO. MinIO's root user cannot be blank. If your .env has AWS_ACCESS_KEY_ID= for local dev, phpunit.xml must override it — env file values and PHPUnit env vars interact in ways that surprise people.
  • Orphaned processes. A crashed test run can leave minio holding port 9000, and the next run fails with a bind error rather than anything to do with your code. Check with lsof -i :9000 before blaming the package. Prefer a trait that registers tearDown/shutdown cleanup, and verify it does.
  • Log and data directories. MinIO writes to a data dir and, by default, logs to stdout. If the trait swallows the process output, you lose the only useful debugging signal. Reproduce outside the suite: run minio server /tmp/minio-data --address :9000 by hand, point config at it, and iterate from there.
  • Binary architecture mismatch. An arm64 binary baked into a Docker image won't run on an x86 CI runner, and vice versa. This is the #1 reason a suite that worked locally fails in CI with a cryptic exit code from Symfony\Process.
  • Low bus factor, near-zero adoption. 15 stars, 0 dependents, and a February 2026 release from a small org. It's ~a few hundred lines of process orchestration — read the source before you depend on it (it's small enough to read in one sitting), and isolate it behind your own S3TestCase so you can drop in a docker compose up -d minio in setUp if it ever goes stale. Do not let the trait name leak into 200 test files.
  • Publish the config and read it. Config keys for port, binary path, data directory, and bucket name are the extension points. A custom binary_path lets you point at the minio already on PATH instead of downloading per-project.
  • It's a dev dependency for a reason. Nothing here should touch require. Also keep the endpoint config in phpunit.xml/config/testing/, not in .env.example, or someone will eventually ship it.
  • Don't use it where Storage::fake() is fine. Asserting Storage::disk('s3')->assertExists(...) after a put() needs no server at all. Reserve MinIO for signing, streams, multipart, visibility, and SDK-level behaviour — that's where the fidelity is worth the seconds.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource