Setting and Testing Cookies in a Livewire Component

I had a need today for a Livewire component to set a cookie, and wanted to test that it was actually set correctly.

Livewire includes support for reading cookies, but not for writing them.

And unfortunately, the redirect helper method doesn’t include any way to set a cookie.

Thankfully, Laravel provides a Cookie::queue() method that will attach set the cookie on the next outgoing response, and since Livewire method calls result in a HTTP response (unless you use the renderless attribute), the framework takes care of attaching the cookie for you:

Cookie::queue('name', 'value', $minutes);

However, I found it counterintuitive to test this behavior.

There is an assertCookie() method available when testing the component, but it always fails because we’re testing a Livewire component, not a request, and so the framework doesn’t attach the queued cookie(s).

My solution: use Cookie::queued() to retrieve the queued cookie, and then run assertions against that:

Laravel 11, Pennant, and Conditional Scheduled Jobs

TL;DR: pass a function instead of Feature::active(…) to your console jobs’ ->when(…) method.

I recently upgraded an app from Laravel 10 to Laravel 11 (I know, I know…I’m a few months behind).

This app was using Laravel Pennant to conditionally register some jobs, based on feature flags:

Schedule::job(GetNewOrders::class)
    ->when(Feature::active(GetOrders::class))
    ->everyFiveMinutes();

Laravel Shift moved these jobs from app/Console/Kernel.php to routes/console.php as expected for the new Laravel 11 structure.

However, for every test that I ran, I was getting these errors:

Base table or view not found: 1146 Table 'test_database.features' doesn't exist

I spent a bit of time troubleshooting and verifying that the migration existed, the database schema was squashed, etc. I was expecting it to fail during the setUpTraits() step of booting the framework, but it actually failed while booting the application. I stepped through more of the setup steps (thanks, xdebug!) and realized that it failed while discovering commands.

That prompted me to comment out the ->when(Feature::active(…)) lines, and voila! my tests suddenly worked!

The when() method accepts either a boolean or a Closure, so I tried wrapping the feature flag in a closure, and my tests still worked:

Schedule::job(GetNewOrders::class)
    ->when(fn () => Feature::active(GetOrders::class))
    ->everyFiveMinutes();

It appears that if your scheduled job ->when(…) conditions depend on the database, you’ll want to wrap them in a function so they aren’t evaluated until they’re actually needed, after the database has already been set up.

Laracon 2024: Nuno Maduro: Pest 3

In his talk, Nuno Maduro announced Pest v3, with several new features and no breaking changes.

I know this is pretty exciting to a lot of people, but personally I prefer PHPUnit for several reasons:

  • Better IDE support
    • e.g., in a Pest test, $this->assertDatabaseHas(…) runs without errors, but the IDE thinks it’s invalid because the test file doesn’t extend the base TestCase or inherit any of the parent methods
    • I personally like the discoverability of using a class, as my IDE will suggest available methods.
    • I do think there are some Pest helpers that I haven’t fully used, so those might help alleviate these gripes.
    • Yes, I know that Pest can be used as the test runner, and still use PHPUnit-style classes.
  • Improved VS Code test integration
    • This is the best VS Code extension I’ve found to work with tests in a PHP project. It shows all the tests along with pass/fail results in the sidebar, and I use the keyboard shortcut to run tests all the time.
    • The extension only works with PHPUnit tests, not Pest.
    • However, the upcoming first-party Laravel VS Code extension might change that…

These new features did catch my eye though:

  • Architecture presets: ensure your code follows best practices and conventions; read more here and see the plugin here
  • Mutation testing: let Pest modify parts of your code to ensure it causes failing tests, to ensure your tests are covering what you think they are; see the plugin here

I’ll be taking a look to see if I can use some of the underlying packages with PHPUnit.

All in all, seems like a solid upgrade, but it just doesn’t excite me all that much.

Fatal Errors with PHPUnit Test Suite: Laravel 10, PHPUnit 10, ParaTest 7

TL;DR: PHPUnit 10.5.32 is broken, 10.5.31 works.

After a composer update one one of my projects today, I started getting this error when running my test suite: Fatal error: Uncaught Illuminate\Contracts\Container\BindingResolutionException: Target [Illuminate\Contracts\Debug\ExceptionHandler] is not instantiable.

All the tests passed just fine…no errors or warnings. At the end of the test execution output, it displayed this stacktrace:

> php artisan test --parallel --stop-on-failure --stop-on-error
ParaTest v7.4.5 upon PHPUnit 10.5.32 by Sebastian Bergmann and contributors.

Processes:     10
Runtime:       PHP 8.3.10
Configuration: /Users/andrewminion/Sites/site-domain/phpunit.xml

.......................................................SS....   61 / 1014 (  6%)
.............................................................  122 / 1014 ( 12%)
.............................................................  183 / 1014 ( 18%)
.............................................................  244 / 1014 ( 24%)
.............................................................  305 / 1014 ( 30%)
.............................................................  366 / 1014 ( 36%)
..................................S...I......................  427 / 1014 ( 42%)
.............................................................  488 / 1014 ( 48%)
.............................................................  549 / 1014 ( 54%)
.............................................................  610 / 1014 ( 60%)
.............................................................  671 / 1014 ( 66%)
.............................................................  732 / 1014 ( 72%)
.............................................................  793 / 1014 ( 78%)
.............................................................  854 / 1014 ( 84%)
.............................................................  915 / 1014 ( 90%)
.........................I...................................  976 / 1014 ( 96%)
......................................                        1014 / 1014 (100%)

Time: 02:04.815, Memory: 70.50 MB

OK, but there were issues!
Tests: 1014, Assertions: 6462, Skipped: 3, Incomplete: 2.

Fatal error: Uncaught Illuminate\Contracts\Container\BindingResolutionException: Target [Illuminate\Contracts\Debug\ExceptionHandler] is not instantiable. in /Users/andrewminion/Sites/site-domain/vendor/laravel/framework/src/Illuminate/Container/Container.php on line 1126

Illuminate\Contracts\Container\BindingResolutionException: Target [Illuminate\Contracts\Debug\ExceptionHandler] is not instantiable. in /Users/andrewminion/Sites/site-domain/vendor/laravel/framework/src/Illuminate/Container/Container.php on line 1126

Call Stack:
  125.2423   77506896   1. Illuminate\Foundation\Bootstrap\HandleExceptions->Illuminate\Foundation\Bootstrap\{closure:/Users/andrewminion/Sites/site-domain/vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php:254-256}() /Users/andrewminion/Sites/site-domain/vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php:0
  125.2423   77507336   2. Illuminate\Foundation\Bootstrap\HandleExceptions->handleException() /Users/andrewminion/Sites/site-domain/vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php:255
  125.2423   77470472   3. Illuminate\Foundation\Bootstrap\HandleExceptions->getExceptionHandler() /Users/andrewminion/Sites/site-domain/vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php:183
  125.2423   77470472   4. Illuminate\Foundation\Application->make() /Users/andrewminion/Sites/site-domain/vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php:288
  125.2423   77470472   5. Illuminate\Foundation\Application->make() /Users/andrewminion/Sites/site-domain/vendor/laravel/framework/src/Illuminate/Foundation/Application.php:946
  125.2423   77470472   6. Illuminate\Foundation\Application->resolve() /Users/andrewminion/Sites/site-domain/vendor/laravel/framework/src/Illuminate/Container/Container.php:731
  125.2423   77470472   7. Illuminate\Foundation\Application->resolve() /Users/andrewminion/Sites/site-domain/vendor/laravel/framework/src/Illuminate/Foundation/Application.php:961
  125.2423   77470528   8. Illuminate\Foundation\Application->build() /Users/andrewminion/Sites/site-domain/vendor/laravel/framework/src/Illuminate/Container/Container.php:795
  125.2423   77470624   9. Illuminate\Foundation\Application->notInstantiable() /Users/andrewminion/Sites/site-domain/vendor/laravel/framework/src/Illuminate/Container/Container.php:921


Fatal error: Uncaught Illuminate\Contracts\Container\BindingResolutionException: Target [Illuminate\Contracts\Debug\ExceptionHandler] is not instantiable. in /Users/andrewminion/Sites/site-domain/vendor/laravel/framework/src/Illuminate/Container/Container.php on line 1126

Illuminate\Contracts\Container\BindingResolutionException: Target [Illuminate\Contracts\Debug\ExceptionHandler] is not instantiable. in /Users/andrewminion/Sites/site-domain/vendor/laravel/framework/src/Illuminate/Container/Container.php on line 1126

Call Stack:
  125.2457   77507224   1. Illuminate\Foundation\Bootstrap\HandleExceptions->Illuminate\Foundation\Bootstrap\{closure:/Users/andrewminion/Sites/site-domain/vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php:254-256}() /Users/andrewminion/Sites/site-domain/vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php:0
  125.2457   77507448   2. Illuminate\Foundation\Bootstrap\HandleExceptions->handleShutdown() /Users/andrewminion/Sites/site-domain/vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php:255
  125.2460   77519232   3. Illuminate\Foundation\Bootstrap\HandleExceptions->handleException() /Users/andrewminion/Sites/site-domain/vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php:231
  125.2460   77519232   4. Illuminate\Foundation\Bootstrap\HandleExceptions->getExceptionHandler() /Users/andrewminion/Sites/site-domain/vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php:183
  125.2460   77519232   5. Illuminate\Foundation\Application->make() /Users/andrewminion/Sites/site-domain/vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php:288
  125.2460   77519232   6. Illuminate\Foundation\Application->make() /Users/andrewminion/Sites/site-domain/vendor/laravel/framework/src/Illuminate/Foundation/Application.php:946
  125.2460   77519232   7. Illuminate\Foundation\Application->resolve() /Users/andrewminion/Sites/site-domain/vendor/laravel/framework/src/Illuminate/Container/Container.php:731
  125.2460   77519232   8. Illuminate\Foundation\Application->resolve() /Users/andrewminion/Sites/site-domain/vendor/laravel/framework/src/Illuminate/Foundation/Application.php:961
  125.2460   77519232   9. Illuminate\Foundation\Application->build() /Users/andrewminion/Sites/site-domain/vendor/laravel/framework/src/Illuminate/Container/Container.php:795
  125.2460   77519328  10. Illuminate\Foundation\Application->notInstantiable() /Users/andrewminion/Sites/site-domain/vendor/laravel/framework/src/Illuminate/Container/Container.php:921

After some trial-and-error, I narrowed it down to PHPUnit 10.5.32. Something in the changeset from 10.5.31 to 10.5.32 caused this error.

This also happens only when running tests in parallel (php artisan test --parallel), suggesting to me that perhaps the PHPUnit output changed in some way that ParaTest was not expecting.

My fix, at least for now, is to stick with PHPUnit 10.5.31, and hopefully an imminent upgrade to Laravel 11 and PHPUnit 11 will resolve it.

Laravel Testing Tip: Reset UUID Creation

TL;DR: if you call Str::createUuidsUsing(…) in a test method, don’t forget to call Str::createUuidsNormally() later in that same method (or the tearDown method), or the rest of your test suite will continue to use that same UUID.

Laravel’s string helper provides a nice interface for generating uuids, as well as a nice way to fake the UUIDs during a test:

While this is very useful, I expected that it would reset between tests, similar to Queue::fake(), Http::fake(), etc.

However, because of how the Str helper generates UUIDs, whatever you provide will be used for the rest of the test run. If you have other tests or app code that expects a unique UUID each time Str::uuid() is used, you may get unexpected results.

There are a couple of options to work around this:

  1. After running the code that needs a UUID, call Str::createUuidsNormally() to reset the Str helper.
  2. If you don’t actually need the value of the UUID for testing, you can wrap your code in the freezeUuids() method instead. Once your code in the callback finishes running, the framework will call createUuidsNormally() to reset everything for you:

Debugging HTTP Client Request Assertions in Laravel Test Suites

The Http::assertSent(), Event::assertDispatched(), and Queue::assertPushed()) test methods are perhaps a bit unintuitive.

They seem like they would run just once and check the assertions provided in your callback.

In fact, that callback runs once for each HTTP request (or dispatched event or queued job) and it evaluates the logic inside the callback for each. So if you have 10 requests (or events or jobs) and 3 of them return true it passes. If you have 10 requests and 9 of them return true it passes. It only fails if none of the callbacks return true.

So using dd($request) in one of those assertions is only dumping out the first one and then killing the test. You might not see the one you actually need.

If you’re trying to see what data is in the request, you’re better off either doing something like Log::debug($request->body()) or ray($request->url())1 or putting an xdebug breakpoint on the first line of the callback, running the test, and pressing the “continue” button until you get to the request you’re trying to inspect (possibly the second or fourth or tenth, depending on what happens before the one you want to inspect).

Here is a few code samples that may clarify this a bit more:

Database Platform Comparisons for Laravel Feature Tests

TL;DR: MySQL significantly outperforms MariaDB in my automated test suite.

The Problem

This Twitter thread prompted me to do a bit of research on database platforms for Laravel automated tests.

I’ve recently been building an ecommerce app based on Laravel. Partway through development, we added geometry fields to a couple of tables in order to determine distances. I’ve been using this spatial package, so SQLite was not an option for my test suite.

As soon as I switched the testing database driver from SQLite to MariaDB, my tests immediately took an extra 12–13 seconds to run, regardless of whether I ran the entire test suite, a single file, or just one test.

This significantly lengthened the feedback loop when making changes to code and re-running tests.

So when I saw Jack Ellis mention that he uses MySQL for his test suite, it made me curious if he had the same issue.

He said that one of his test files runs 39 tests in < 2 seconds, so apparently it’s not been a problem for him.

Context

  • I’m using the LazilyRefreshDatabase trait added in Laravel 8.62.0 on my entire test suite
  • I’m using squashed migrations
  • Many of my tables have constrained foreign keys referencing other tables

Comparisons

I decided to do some digging; here are comparisons using four different platforms for the same test in my application.

MariaDB

I’ve been using MariaDB as the main database platform on my development machine for years. Currently I’m on version 10.6.4.

In-Memory SQLite Database

I temporarily disabled the geometry features and tried the in-memory SQLite database (DB_CONNECTION=:memory:); it performed much better for the same tests:

SQLite File Database

I then tried with an SQLite file (DB_CONNECTION=sqlite), and it performed about the same:

MySQL 8

I have an installation of MySQL 8 set up for one app that uses some specific MySQL 8 and I figured why not give that a try too.

Here are the results:

Summary

For some reason, MariaDB takes approximately 12–13 seconds to tear down and recreate the database before starting to run tests, but MySQL is much faster.

While testing MariaDB, I opened the raw data directory for the database, and noticed chunks of files being removed and recreated at a time, so perhaps the foreign key constraints are (part of) the culprit here.

I do have 77 databases with ~3800 tables in my MariaDB installation built up from various projects over the years. It seems unlikely, but theoretically possible, that the server size could be part of the problem too.

I think I’ll experiment with switching back to MySQL as my development platform of choice.

Have you run into this same issue? Have any tips or tricks? Let me know in the comments.