Reading List
The most recent articles from a list of feeds I subscribe to.
Tabular assertions with PHPUnit
With tabular assertions, you describe the expected outcome in a markdown-like table format. Here's why there useful, and how to use tabular assertions with PHPUnit.
This is a tabular test in PHPUnit:
use PHPUnit\Framework\TestCase;use Spatie\TabularAssertions\PHPUnit\TabularAssertions; class OrderLogTest extends TestCase{ use TabularAssertions; public function test_it_logs_order_activity(): void { $order = OrderFactory::create() ->withPrice(100_00) ->withTaxRates([5_00, 10_00]) ->withShipping(5_00) ->paid(); $this->assertMatchesTable(' | type | reason | #product_id | #tax_id | #shipping_id | #payment_id | price | paid | | product | created | #1 | | | | 100_00 | 0_00 | | tax | created | | #1 | | | 5_00 | 0_00 | | tax | created | | #2 | | | 10_00 | 0_00 | | shipping | created | | | #1 | | 5_00 | 0_00 | | product | paid | #1 | | | #1 | 0_00 | 100_00 | | tax | paid | | #1 | | #1 | 0_00 | 5_00 | | tax | paid | | #2 | | #1 | 0_00 | 10_00 | | shipping | paid | | | #1 | #1 | 0_00 | 5_00 | ', $order->activity); }}
Before we dive into the how-to, lets review why tabular assertions are useful.
Lots of data at a glance
Text-based tables are compact. If we want to assert the same data with assert
methods, we need a lot of vertical space.
$this->assertSame('product', $order->activity[0]->type);$this->assertSame('created', $order->activity[0]->reason);$this->assertSame(100_00, $order->activity[0]->price);$this->assertSame(0_00, $order->activity[0]->paid); $this->assertSame('tax', $order->activity[1]->type);$this->assertSame('created', $order->activity[1]->reason);$this->assertSame(5_00, $order->activity[1]->price);$this->assertSame(0_00, $order->activity[1]->paid); // …and 24 more lines
Regular assertions require you to assert each property individually. In this example, we're asserting 4 properties across 8 rows. 8 * 4 = 32
so that would require 32 separate assertions, and won't scale well. This makes it hard to see all data at a glance, and is less readable in general.
Alternatively, we could use associative arrays or tuples to assert data in bulk.
$this->assertEquals([ ['type' => 'product', 'reason' => 'created', 'price' => 100_00, 'paid' => 0_00], ['type' => 'tax', 'reason' => 'created', 'price' => 5_00, 'paid' => 0_00], // …and 6 more lines], $order->activity);
Associative arrays are more verbose but require a lot of repetition.
$this->assertEquals([ ['product', 'created', 100_00, 0_00], ['tax', 'created', 5_00, 0_00], // …and 6 more lines], $order->activity);
Tuples are more compact and readable at a glance once you understand the shape. However, both suffer from whitespace issues: the columns are not aligned. We could add spaced to align them, but code style fixers don't always like this.
With tabular assertions, we get a compact, readable overview of the data, and because it's stored in a single string code style fixers won't reformat it.
$this->assertMatchesTable(' | type | reason | price | paid | | product | created | 100_00 | 0_00 | | tax | created | 5_00 | 0_00 | | tax | created | 10_00 | 0_00 | | shipping | created | 5_00 | 0_00 | | product | paid | 0_00 | 100_00 | | tax | paid | 0_00 | 5_00 | | tax | paid | 0_00 | 10_00 | | shipping | paid | 0_00 | 5_00 |', $order->activity);
Failures display multiple problems
With separate expectations, tests fail on the first failed assertion which means you don't have the full picture.
$this->assertSame('product', $order->activity[0]->type);$this->assertSame('created', $order->activity[0]->reason);$this->assertSame(100_00, $order->activity[0]->price);$this->assertSame(0_00, $order->activity[0]->paid);
Back to our first example of assertions, when the reason
is wrong the test will fail. Did reason fail for a single row, or are the reasons wrong everywhere? This can be valuable information when debugging which is lost in classic tests.
It could also be that an activity row is missing, which causes all other rows to fail. This doesn't mean they contain wrong data, the assertions are tied to a different index. Tabular testing makes this clear with a diff.
Dynamic placeholders
Sometimes you want to compare data without actually comparing the exact value. For example, you want to assert that each person is in the same team, but don't know the team ID because the data is randomly seeded on every run.
With tabular assertions, you can mark a column as dynamic by prefixing a #
. This will assign each unique value to a placeholder ID, so similar values can be compared.
In our initial example, we don't care about the exact product_id
of the rows as it's a randomly seeded ID we can't assert. We do however care that the created
and paid
activity are tied to the same product, which becomes clear with the placeholders.
$this->assertMatchesTable(' | type | reason | #product_id | #tax_id | #shipping_id | #payment_id | price | paid | | product | created | #1 | | | | 100_00 | 0_00 | | tax | created | | #1 | | | 5_00 | 0_00 | | tax | created | | #2 | | | 10_00 | 0_00 | | shipping | created | | | #1 | | 5_00 | 0_00 | | product | paid | #1 | | | #1 | 0_00 | 100_00 | | tax | paid | | #1 | | #1 | 0_00 | 5_00 | | tax | paid | | #2 | | #1 | 0_00 | 10_00 | | shipping | paid | | | #1 | #1 | 0_00 | 5_00 |', $order->activity);
Setting up tabular assertions with PHPUnit
Tabular assertions can be installed via composer.
composer require spatie/tabular-assertions --dev
Next, add the TabularAssertions
trait to your test case (or a base test case if you want to enable it everywhere.)
use PHPUnit\Framework\TestCase;use Spatie\TabularAssertions\PHPUnit\TabularAssertions; class OrderLogTest extends TestCase{ use TabularAssertions;}
Finally, assert data with the assertMatchesTable
method.
use PHPUnit\Framework\TestCase;use Spatie\TabularAssertions\PHPUnit\TabularAssertions; class OrderLogTest extends TestCase{ use TabularAssertions; public function test_it_logs_order_activity(): void { $order = OrderFactory::create() ->withPrice(100_00) ->withTaxRates([5_00, 10_00]) ->withShipping(5_00) ->paid(); $this->assertMatchesTable(' | type | reason | #product_id | #tax_id | #shipping_id | #payment_id | price | paid | | product | created | #1 | | | | 100_00 | 0_00 | | tax | created | | #1 | | | 5_00 | 0_00 | | tax | created | | #2 | | | 10_00 | 0_00 | | shipping | created | | | #1 | | 5_00 | 0_00 | | product | paid | #1 | | | #1 | 0_00 | 100_00 | | tax | paid | | #1 | | #1 | 0_00 | 5_00 | | tax | paid | | #2 | | #1 | 0_00 | 10_00 | | shipping | paid | | | #1 | #1 | 0_00 | 5_00 | ', $order->activity); }}
Custom assertions
Tabular assertions will cast the actual values to strings. We're often dealing with data more complex than stringables, in those cases it's worth creating a custom assertion method that prepares the data.
Consider the following example with a User
model that has an id
, name
, and date_of_birth
which will be cast to a Carbon
object.
$this->assertMatchesTable(' | name | date_of_birth | | Sebastian | 1992-02-01 00:00:00 |', User::all());
Because Carbon
objects automatically append seconds when stringified, our table becomes noisy. Instead, we'll create a custom assertMatchesUsers
assertion to prepare our data before asserting.
class UserTest extends TestCase{ use TabularAssertions; public function test_it_has_users(): void { $this->assertMatchesUsers(' | name | date_of_birth | | Sebastian | 1992-02-01 | ', User::all()); } private function assertMatchesUsers(string $expected, Collection $users): void { $users = $users->map(function (User $user) { return [ 'name' => $user->name, 'date_of_birth' => $user->date_of_birth->format('Y-m-d'), ]; }); $this->assertMatchesTable($expected, $users); }}
This can also useful for any data transformations or truncations you want to do before asserting. Another example: first_name
and last_name
might be separate columns in the database, but in assertions they can be combined to reduce unnecessary whitespace in the table.
class UserTest extends TestCase{ use TabularAssertions; public function test_it_has_users(): void { $this->assertMatchesUsers(' | name | date_of_birth | | Sebastian De Deyne | 1992-02-01 | ', User::all()); } private function assertMatchesUsers(string $expected, Collection $users): void { $users = $users->map(function (User $user) { return [ 'name' => $user->first_name . ' ' . $user->last_name, 'date_of_birth' => $user->date_of_birth->format('Y-m-d'), ]; }); $this->assertMatchesTable($expected, $users); }}
The full source & documentation for spatie/tabular-assertions
is available on GitHub.
Introducing tabular assertions
Today I tagged v1 of a new testing package: spatie/tabular-assertions
. It's a distillation of a testing method I've been using in client projects the past two years. The package supports both PHPUnit and Pest out of the box.
With tabular assertions, you describe the expected outcome in a markdown-like table format.
expect($order->logs)->toLookLike(" | type | reason | price | paid | | product | created | 80_00 | 80_00 | | tax | created | 5_00 | 5_00 | | tax | created | 10_00 | 10_00 | | shipping | created | 5_00 | 5_00 | | product | paid | 0_00 | 0_00 | | tax | paid | 0_00 | 0_00 | | tax | paid | 0_00 | 0_00 | | shipping | paid | 0_00 | 0_00 |");
Tabular assertions have two major benefits over other testing strategies: expectations are optimized for readability & failed assertions can display multiple errors at once.
For an in-depth introduction to tabular testing, I've written two separate guides for Pest & PHPUnit.
Inspiration
I haven't come across this exact method anywhere else, so I had to come up with a name. If there's prior art that matches this with a better name, I'd love to know!
The idea was inspired by Jest, which allows you to use a table as a data provider.
Snapshot testing is also closely related to this. But snapshots aren't always optimized for readability, are stored in a separate file (not alongside the test), and are hard to write by hand (no TDD).
Tabular assertions have been a huge help when comparing large, ordered data sets like financial data or a time series. I hope you find it useful too!
Dan Abramov: The two Reacts
Frontend frameworks often use the mental modal that a component (or entire UI) is the result of running state through a pure function. Dan Abromov shares his thoughts on the current state of React with server components.
UI = f(data)(state)
Here's how I interpret it: in the mental model we're used to, a component is a pure function that returns UI based on its props. In this case, a Greeter
is rendered on the client.
function Greeter(props) { return <p>Hello, {props.name}!</p>;}
Say we have a server component that know who needs to be greeted before the code hits the client. In that case, the name will come from data available on the server.
function Greeter(data) { return function () { return <p>Hello, {data.name}!</p>; }}
The outer function is executed on the server, the inner on the client.
The true power lies in mixing and matching the paradigms. Say you want to read a translation string on the server, and greet a name on the client.
function Greeter(data) { return function (props) { return <p>{data.translations.hello}, {props.name}!</p>; }}
Feedback
Before I launched Svelte by Example, I called for early access testers in this newsletter. I don't ask for feedback often, I had to push myself to do this.
Sometimes I'm scared of receiving feedback that I'll agree with, but would push back the release when I want to get it out. Sometimes I'm scared of receiving feedback that could invalidate the entire idea. Sometimes I know there are problems but hope they'll magically go away if I ignore them, feedback might resurface them. Sometimes I'm overconfident and don't think it's worth getting another opinion.
No good excuse to be found. These are fears, and it's worth getting over them. Because if any of them are rooted in truth, they'll com back and haunt me rather sooner than later.
A few people responded to my request (thank you!), and it quickly became clear the ask was worth it. The first version of Svelte by Example became way better because of it. The examples became more consistent, the design improved, a bunch of typos were edited out. I didn't process all feedback. Sometimes it doesn't match what you have in mind, and that's fine.
I've learned my lesson: time to get over myself and ask for feedback whenever I can.
Svelte 5 preview review
The Svelte team set up a miniature documentation site highlighting the new features coming to Svelte 5. Here's a quick review of my three favorites: runes, events, and snippets.
Runes
The star of the release will be runes. Currently, Svelte uses existing language features like export
and labels for its reactive features.
<script> // A prop export let name; // A reactive statement $: console.log(name);</script> <p>Hello, {{ name }}!</p>
Svelte 5 is a departure from this setup, and has more explicit "runes" instead. Runes are function calls that get transformed by the compiler, and can be recognized by their $
prefix.
<script> // A prop let { name } = $props(); // A reactive statement $effect(() => { console.log(name); });</script> <p>Hello, {{ name }}!</p>
I personally liked the (mis)use of existing JavaScript syntax for reactivity. However, it did come with weird edge cases which runes will iron out, and having one "system" for all reactivity features does make Svelte look more consistent. So all in all I understand why the team decided to pursue this direction, and I'm looking forward to trying it out.
Events
Event handles in Svelte have previously been prefixed with on:
. This prefix has been removed and replaced with a syntax that mirrors props.
<!-- Svelte 4 --><button on:click={() => …}> …</button> <!-- Svelte 5 --><button onclick={() => …}> …</button>
This makes component events more straightforward too as events are handled by callback props.
<!-- Svelte 4 --><Todo on:complete={() => …} /> <!-- Svelte 5 --><Todo complete={() => …} /> <!-- Todo.svelte --><script> let { complete } = $props();</script> <input type="checkbox" oninput={complete} />
This opens up a world of possibilities, as you can now dynamically spread props and events on a component, which is something I sorely miss coming from React.
We often use form objects in projects that hold form state, and have methods to bind form values, handlers, and errors on a field component in a type-safe way. This is finally possible in Svelte.
<script> let form = createForm({ name: 'Sebastian' });</script> <TextField {...form.field('name')} />
Snippets
This is an interesting one I haven't seen in other frameworks yet, but solves a problem I've had in other templating languages. Snippets allow you to reuse an HTML snippet within the same component file.
Here's an example that would be relevant to my blog: I have a PostHeader
component that's used on the index page and post page. On the index page, the title should be wrapped in an h2
tag, on the post page in an h1
.
{#if onPostPage} <h1><a href="{{ permalink }}">{{ title }}</a><h1>{:else} <h2><a href="{{ permalink }}">{{ title }}</a><h2>{/if}
This is a small example, but grows increasibly difficult to maintain as the inner contents of the dynamic tag grows. In Svelte 5, you can extract the inner contents to a snippet.
{#snippet title()} <a href="{{ permalink }}">{{ title }}</a>{/snippet} {#if onPostPage} <h1>{@render title()}<h1>{:else} <h2>{@render title()}<h2>{/if}
Now there's no more repetition of the snippet contents.
There's more on the horizon for Svelte 5, read through the preview documentation for an introduction to all new features. Or if you're new to Svelte and want an introduction, check out Svelte by Example. (Which I'll update for Svelte 5 when it's released!)