k2gl/phpunit-fluent-assertions
A PHPUnit extension that adds fluent, chainable assertions to your tests, making expectations more readable and expressive while reducing verbose assertion code and keeping familiar PHPUnit workflows.
Install as a dev dependency:
composer require --dev k2gl/phpunit-fluent-assertions
Confirm the package major matches your PHPUnit major in composer.json — this library tracks PHPUnit’s assertion API, so a mismatch is the most common install failure.
Where to look first:
README.md usage section for the Assert::that() entry point.src/Assert.php (or equivalent) to see how __call maps fluent methods to PHPUnit’s assert* methods (assertIsString() → isString(), assertStringContainsString() → contains()).First use case — replace verbose PHPUnit assertions in a Laravel test:
use K2gl\PHPUnitFluentAssertions\Assert;
// before
$this->assertIsString($user->name);
$this->assertStringContainsString('a', $user->name);
$this->assertNotEmpty($user->email);
// after
Assert::that($user->name)->isString()->contains('a');
Assert::that($user->email)->isNotEmpty();
Start with one test class, convert a handful of assertions, and run the suite. The fluent API is additive; you do not need to convert everything at once.
Chain assertions on a single subject. The main value is readability when several checks apply to the same value:
Assert::that($response->json('data'))
->isArray()
->hasKey('id')
->hasKey('email');
Use negation sparingly. not() flips the next assertion:
Assert::that($payload)->not()->isEmpty();
Assert::that($user->deleted_at)->isNull();
Exception testing (updated in 12.12.0). The new doesNotThrow() and the inspect callback for throws() make exception assertions fit the fluent style:
// happy path: assert the callable completes without throwing
Assert::that(fn () => $this->service->charge($order))->doesNotThrow();
// assert the exception class and inspect it
Assert::that(fn () => $this->service->charge($order))
->throws(InsufficientFundsException::class, function (InsufficientFundsException $e) {
Assert::that($e->getMessage())->contains('balance');
Assert::that($e->getCode())->isSame(402);
});
Laravel feature tests. Use it for response status, JSON shape, and model state:
$response = $this->postJson('/api/users', $payload);
Assert::that($response->status())->isSame(201);
Assert::that($response->json('data'))->isArray()->hasKey('id');
Assert::that(User::count())->isSame(1);
Pest tests. Assert::that() works inside Pest closures; you can keep Pest’s expect() for higher-order tests and use fluent assertions where chaining reads better. Avoid mixing both styles on the same value in one test.
Reusable domain assertions. Wrap repeated chains in a trait or base TestCase method:
protected function assertValidUserPayload(array $payload): void
{
Assert::that($payload)
->hasKey('name')
->hasKey('email');
}
Extension points. If the package exposes a macro/extension mechanism (check Assert::macro or the Assert class for a registration method), register custom assertions in a test-only service provider’s boot() method. Otherwise, subclass Assert or wrap it in your own helper.
__call, so IDEs and static analysers do not know them. Check whether the package ships PHPStan/Psalm stubs or @method annotations; if not, add a local stub or docblock to avoid a wall of “undefined method” warnings.assertSame() when you need maximum debugging context.doesNotThrow() catches Throwable, including PHPUnit assertion failures. If the callable contains its own assertions that fail, PHPUnit throws AssertionFailedError, which doesNotThrow() will report as an unexpected exception. Keep assertion logic outside the callable or assert on the returned value instead.throws() inspect callback runs after the exception is caught. Type-hint the callback parameter to the expected exception class; if the callback throws, that exception propagates and can mask the original failure. Keep the callback focused on inspecting the exception, not on unrelated assertions.composer.json constraint before upgrading either side.not() applies to the next assertion only; it does not permanently flip the object. For multiple negative assertions, call not() each time or use explicit positive assertions where clearer.How can I help you explore Laravel packages today?