nunomaduro/laravel-mojito
Laravel Mojito is a lightweight testing helper by Nuno Maduro that simplifies mocking Laravel facades. Define expectations and return values with a clean, fluent API, keeping tests fast and readable without boilerplate.
Install (dev-only):
composer require --dev nunomaduro/laravel-mojito
Because the last release is from April 2021, check the composer.json constraints before committing to it. It was built for PHP 7.x and Laravel 5.5–8; on Laravel 9+ you will likely need to fork it or skip it (see Gotchas). v0.2.8 adds an empty assertion but is otherwise a patch-level release — no breaking changes, no new requirements, so an existing pin to ^0.2 picks it up for free.
Where to look first:
README.md for the assertion API surface — it is a very small package, so the docs are the best map. As of v0.2.8 the surface is contains(), doesNotContain(), and empty().src/Mojito.php and the assertion/test-case classes — the whole package is a few hundred lines, and reading it is faster than guessing at behaviour.tests/ directory — these are the most reliable, up-to-date examples of supported usage. Check whether the empty assertion landed with a test; if it did not, that tells you something about the release's maturity.First use case — assert a Blade view renders in isolation:
The core idea is that you should not need a full HTTP feature test to know that resources/views/welcome.blade.php renders the name you passed it. Instead:
use NunoMaduro\Mojito\Tests\TestCase;
class WelcomeViewTest extends TestCase
{
public function test_it_renders_the_name(): void
{
$this->assertViewRenders('welcome', ['name' => 'Nuno Maduro'])
->contains('Nuno Maduro');
}
}
Start by migrating one or two "view-shaped" assertions out of a slow Feature test into a Mojito test. That gives you an immediate feel for the boot cost and the fidelity trade-off before you convert a whole suite.
1. One test per view, data-driven with fixtures.
The sweet spot is a table of data arrays per view, asserted in a loop:
/**
* @dataProvider welcomeProvider
*/
public function test_welcome_renders(array $data, string $expected): void
{
$this->assertViewRenders('welcome', $data)->contains($expected);
}
public function welcomeProvider(): array
{
return [
'named user' => [['name' => 'Nuno'], 'Nuno'],
'empty name' => [['name' => ''], 'Welcome'],
'admin flag' => [['name' => 'A', 'admin' => true], 'Admin'],
];
}
This is where Mojito earns its keep: the failure message points at a view + data combination, not at a route, middleware stack, or session state.
2. Separate "does the view render" from "does the endpoint work".
A useful split in a mature suite:
Feature tests then assert one happy-path render and stop duplicating every @if branch. This is the highest-leverage change you can make with this package.
3. Assertion chains for presence, absence, and emptiness.
Pair contains() with its negative counterpart to lock down conditional rendering:
$this->assertViewRenders('dashboard', ['admin' => false])
->doesNotContain('Admin panel');
$this->assertViewRenders('dashboard', ['admin' => true])
->contains('Admin panel');
Treat doesNotContain as the more valuable assertion — it catches accidentally leaked markup far more often than positive assertions catch missing markup.
4. Use empty() for views that are supposed to render nothing.
The v0.2.8 addition is the answer to a real annoyance: proving a negative. Conditional partials, x-slot-driven wrappers, mail fragments that only appear for some users, and JSON-LD/analytics snippets gated behind config all have a "renders nothing at all" state that is awkward to express with doesNotContain (you end up guessing at strings that might not exist anyway).
$this->assertViewRenders('partials.admin-badge', ['admin' => false])->empty();
$this->assertViewRenders('partials.admin-badge', ['admin' => true])
->contains('Admin');
Use it as the negative half of a pair, the same way you use doesNotContain. It is a stronger claim than "this string is missing" — it asserts the whole render produced no output — so it belongs on small, single-purpose templates rather than on full pages, which almost always emit surrounding whitespace or layout markup. Verify against your installed version before relying on it in a shared suite: the method is new in 0.2.8, and older pins in a lockfile or a CI image will fail with a BadMethodCallException.
5. Use it for Blade component and partial work.
When you are iterating on a single component (x-alert, a mail partial, a PDF template), a Mojito test gives you a sub-second feedback loop without touching a controller. This is the closest thing to "unit testing a view" that Laravel offers out of the box.
6. Combine with snapshot testing for whole-output regression.
Mojito tells you a string is present; it does not tell you the layout did not shift. For views you care about visually, pair it with a snapshot/spatie-style assertion on the full rendered HTML, and keep Mojito for the semantic checks. Snapshots also cover the empty() case from the other direction — a snapshot of '' is unmissable in review.
7. Run it as its own CI lane.
Because each test renders a single view rather than booting a request, these run fast. Group them under a separate PHPUnit testsuite (e.g. tests/Views) so a broken view fails fast in CI before the feature suite runs.
Maintenance is the headline risk. Last release 2021-04-08 (v0.2.8), zero dependents, low overall score. v0.2.8 is a single-assertion patch, not a sign of revival — assume the compatibility floor has not moved. Before adopting, run the suite against your Laravel version. Common failure modes on Laravel 9+:
BladeCompiler::compileString, component resolution) break rendering.If you hit these, the package is small enough to copy the ~200 lines into tests/Support/ and own it. That is often cheaper than a fork with a Composer vcs repository entry. If you do vendor it, port empty() too — it is a handful of lines and removes the most common reason to reach for a brittle doesNotContain guess.
empty() is whitespace-sensitive. It asserts the render produced nothing, so a template ending in a newline after @endif, or a leading blank line from a @php block, will fail it. Trim the template or assert on a narrower partial. When it fails confusingly, dump the render (see below) — the culprit is usually invisible whitespace, not unexpected markup.
Reduced application fidelity is by design. Mojito boots a lightweight container, so anything resolved at render time may behave differently than in a real request:
auth()->user(), Gate::allows(), and policy checks — inject stubs or use actingAs if supported.View::share() from service providers may not be registered. If a view depends on globally shared data, pass it explicitly in the data array instead of relying on the composer.config(), route(), url(), and localization helpers can be partially wired; a missing route name will throw at render time, not at assert time.This matters doubly for empty(): a view whose only content comes from a view composer will render empty under Mojito and non-empty in production. A passing empty() test can therefore be a false positive if the content source is shared data rather than template logic. Prefer empty() on partials that branch on passed-in data.
Blade components and slots need explicit data. <x-*> components resolve their own class and may require constructor dependencies; if a component injects a service, the minimal container can fail to resolve it. Test the component's Blade file directly with plain data, or fall back to a normal feature test for component-heavy views.
Debugging a failing assertion is not obvious. The assertion output often shows only "string not found" without the rendered HTML. Get in the habit of dumping the render first:
$html = view('welcome', ['name' => 'Nuno'])->render();
dump($html); // then write the Mojito assertion against what you actually see
If view()->render() and the Mojito render disagree, that difference is the fidelity gap — usually a missing composer or shared variable. For empty(), dump(strlen($html)) is quicker than eyeballing whitespace.
Escaping will bite you. contains() matches raw output, so Nuno & Sons renders as Nuno & Sons. Assert against the escaped form, or you will chase phantom failures.
Whitespace and formatting are literal. Multi-line Blade output contains the indentation from the template. Prefer short, distinctive strings ('Admin panel') over full sentences that a formatter or @php block can reflow.
Extension points are minimal — that is the point. There is no plugin system. If you need a custom assertion, subclass the test case or write a trait wrapping assertViewRenders. Do not expect to hook into the renderer.
Have a no-dependency fallback ready. If compatibility becomes a burden, the 90% substitute is:
$this->assertStringContainsString('Nuno', view('welcome', $data)->render());
$this->assertSame('', view('partials.admin-badge', $data)->render());
It lacks the ergonomics and the isolation framing, but it has zero maintenance risk and works on every Laravel version. Evaluate Mojito on whether the assertion API and failure ergonomics are worth the pinning cost for your team — for a 367-star, four-year-stale package, that bar is high, and one new assertion does not change it.
How can I help you explore Laravel packages today?