mindplay/testies
Testies is a lightweight PHP testing framework with a clean, expressive API for writing and organizing tests. It offers test suites, assertions, mocking support, and a simple runner, making it a compact alternative to heavier tools while fitting easily into existing PHP projects.
Install as a dev dependency only:
composer require --dev mindplay/testies
There is no service provider, no config file, no Laravel integration layer. It is a flat set of namespaced functions (test(), run(), and a bag of assertions like eq(), ok(), throws()) that get imported with use function. That's the whole mental model — treat it as a script runner, not a framework.
Where to look first: the package README, then vendor/mindplay/testies/src/ — it's small enough to read in one sitting, and you'll want to know exactly which assertion functions exist because there's no IDE-friendly fluent API to discover (expect(...)->toBe(...) does not exist here).
First use case — don't start with the app. The right first fit in a Laravel codebase is a self-contained unit: a value object, a pure helper, a string/number formatter, or an internal packages/ library you haven't wired into the framework yet. Write one file, import functions, define cases, call run():
<?php
require __DIR__ . '/../vendor/autoload.php';
use function mindplay\testies\{test, run, eq, ok, throws};
test('Money normalizes to cents', function () {
eq(Money::fromString('12.30')->cents(), 1230);
});
test('Money rejects negative values', function () {
throws(fn() => Money::fromString('-1.00'), InvalidArgumentException::class);
});
run();
Execute with php tests/money.php. If it exits non-zero on failure (it does), it already drops into CI without ceremony.
Pattern 1 — Standalone package tests. For an internal composer package (packages/billing, packages/geo), commit a tests/run.php and wire it up:
"scripts": {
"test:geo": "php packages/geo/tests/run.php"
}
This gives you fast feedback without booting PHPUnit's bootstrap or Laravel's kernel. Good for pure logic where the framework would only add latency.
Pattern 2 — Bootstrapping Laravel manually in a script. You can still exercise container-resolved services from a plain testies script; you just do the bootstrapping yourself:
$app = require __DIR__ . '/../bootstrap/app.php';
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
test('ReportBuilder totals line items', function () use ($app) {
$builder = $app->make(ReportBuilder::class);
eq($builder->total([100, 250, 50]), 400);
});
Use this only for narrow service-level checks. The moment you need RefreshDatabase, HTTP assertions, actingAs(), or queue/event fakes, you're in PHPUnit/Pest territory — don't fight it.
Pattern 3 — Smoke tests for CLI scripts and one-off commands. artisan commands that are hard to test through the standard TestCase (long-running loops, filesystem side effects) can be shelled out and verified with ok() / eq() on their output. This is often faster to write than mocking the console kernel.
Pattern 4 — Grouping by file, not by suite. Without describe() or data providers, the idiom is one test file per unit, many test() calls inside, run() at the bottom. If you need shared setup, do it in a closure above the test() calls — there's no setUp()/tearDown() lifecycle to lean on.
Pattern 5 — Mixing with PHPUnit deliberately. You can keep PHPUnit as the default composer test and add a composer test:units script for testies-run micro-suites. Splitting by kind of test (framework-coupled vs. pure) is cleaner than trying to force one runner everywhere.
use function. assert() in particular collides with PHP's built-in (and PHPUnit's), and is(), eq(), ok() are generic enough to clash in big codebases. Always import explicitly (use function mindplay\testies\{test, run, eq};) rather than relying on loose globals, and never require two assertion libraries into the same file.setUp, no mock(), no dataset(). If you catch yourself writing a hand-rolled for loop to feed cases into a test(), you've outgrown the tool for that file — move it to PHPUnit where data providers exist.run() must be called explicitly, and it's the thing that emits results and sets the exit code. Forgetting it means a green build that tested nothing. Guard against this by having a single run() per entry-point file and nothing else at the top level.env(), config cache, service bindings) must exist before the assertions run. Load .env and bootstrap the kernel at the top of the file, before test() definitions, or you'll get confusing "value is null" failures that are really bootstrap failures.--coverage flag. If you need coverage numbers, wrap the invocation: XDEBUG_MODE=coverage php -d ... tests/run.php and feed the output to your normal coverage tooling, or just accept that these micro-suites are for correctness, not metrics.eq(json_encode($actual), json_encode($expected))) so CI logs are actually readable.composer.json and be prepared to vendor it or replace it — the surface area is small enough that a self-written eq()/throws() helper is a realistic fallback.run() are the idiomatic way to add custom assertions. Keep them in a shared tests/support.php required by each script so the "framework" you build stays consistent across suites.How can I help you explore Laravel packages today?