Chris Padilla/Blog
My passion project! Posts spanning music, art, software, books, and more.
- If a client is found, add the patch data.
- If not, create a new client.
- Alex Gusman has a great walk through of his own experience using playwright and preferring e2e testing.
- Many of Kent C. Dodd's articles on testing have informed my thoughts here. Most relevant may be How To Know What To Test.
- I've been reading Mark Richards' and Neal Ford's Fundamentals of Software Architecture. They preface that any technical conversation starts off with "It Depends." So this article is just another big "It Depends" conversation.
- Speaking of, I love this faux O'Reilly cover also called "It Depends."
- First, you set some stuff up (“Arrange”)
- Then, you do something (“Act”)
- Then, you make sure that what you expected to happen, actually happened. (“Assert”)
- Converting your data to JSON
- Passing the ID as a query param
- Comparing between the string representation of the ID and the
ObjectID
data type
Yesterdays
Days I knew as happy, sweet, sequestered days~
Donut Tubing
Been trying out 3D graphics in Blender! Donut courtesy of Andrew Price's tremmendous material.
Tending the Garden With Tag Pages
I spent last week doing some digital gardening. I have a couple of years of posts on this blog minimally tagged, only marking them for large buckets like "Tech", "Music", "Art", etc. I wanted to start threading themes, in part to increase the topography of the blog1. (See my write up on back links for a deeper explanation.)
Another benefit is getting to really take in a couple of years of posts and bundle them together. They become more than just one-off posts and now become projects that I can name and explain!
Tag Pages
You'll see the fruits of my labor on tag pages. For example, on the Music tag page, I've surfaced these tags:
Clicking each of those takes you into a more refined look at what I've shared. And those tag pages have their own set of tags that can be linked to! For example, on the Piano tag page, I have a list of tags that includes Chris Learns Piano. So thee's a fun bit of refinement that can be done in the search as you click into it.
On special tags that I feel benefit from an explanation, I can provide it. It's a nice entry point and description for a collection. Using "Chris Learns Piano" as an example, I've included a quick blerb:
I've been dabbling most of my life, but I decided to really go in on learning piano in 2022.
Just enough to give context to what's pulled up.
Projects
Bringing loosely related posts under tags also helps with encapsulating them into "projects" rather than streams. There's an important distinction there. I got to thinking about it after Dave Rupert shared his own realization: It's important for projects to have end dates.
Here I'll refference my art tages Chris Learns to Draw and Chris Learns Digital Painting. Over on the tag page is an explanation of how I've taken the past couple of years to learn drawing and ditial painting from scratch. Context that I don't share with every post.
I'm planning on keeping up drawing! However, I want to put a bow on those projects. They are horizons to move towards, certainly. But along the way, that horizon needs a few land markers and pit stops. They free my mental energy to chose whatever is next conciously.
Interestingly, it's retroactive grouping instead of an initial deadline. I prefer this. It balances the benefit of both a routine feed of what's happening and a more defined project. By sharing what your working are and then later scooping up different peices into a cohesive collection, you get both flexibility and structure. Plenty of people write books this way, in fact!
Grouping my peices up also gives a big confidence boost! It feels good looking back and saying that I completed a couple of year-long learning projecs!
Favorite Tags
I have a few favorite collections that have emerged.
Number one has to be Lucy, a brief collection of posts where our sweet pup makes an appearance. (She's a regular subject in paintings!)
I don't often have the opportunity, but when I play an acoustic piano, I always savor it.
It's probably evident from what I share, but I do a ton of reflecting. I take every chance I get to do annual reflections for different milestones. I like marking occassions and celebrating with writing.
Tending
A garden needs tending. Harvesting and reworking. Seeing what's grown, knowing what to continue watering. The benefit of a long term project like a blog is seeing what sprouts. Seeing development over time. To only post and never organize would miss most the benefit. Revisiting and organizaing brings a cohesion to the organic growth.
Test Deliverable Outcomes
Opinions around testing in software are varied. Do you go with the Pyramid shape or the trophy approach? Do you adopt Test Driven Development or lean more heavily on a few critical end-to-end tests after the code is written?
This past month, I spent time working on a side project that explores all the tests working hand-in-hand. I wanted the ideal scenario: 100% test coverage with a full suite of unit, integration, and e2e testing. Let's see if the juice was worth the squeeze!
The App
My target for testing is this upsert page for a client management portal. We'll focus on a simplified version of this page, utilizing React Hook Form to control the form, Mantine to style, and Zod for client side validation.
export default function ClientUpsert({
client = undefined,
newClient = false
}: {
client?: Client;
}) {
const router = useRouter();
const {
register,
handleSubmit,
formState: { errors, dirtyFields, isSubmitting, disabled, isValid },
control
} = useForm<Client>({
values: client,
resolver: zodResolver(ClientUpsertSchema.partial()),
mode: 'onBlur'
});
const onSubmit: SubmitHandler<Client> = async (data) => {
// Truncating submit logic for brevity's sake.
upsertClient(id, data);
};
return (
<Container>
<h1 role="heading">New Client</h1>
<form onSubmit={handleSubmit(onSubmit)} key="upsert-client">
<h2 role="heading">{client?.calculated?.fullName}</h2>
<Stack gap="lg">
<TextInput
{...register('profile.firstName', { required: true })}
label="First Name"
error={errors.profile?.firstName?.message as string | undefined}
role="textbox"
/>
<TextInput
{...register('profile.lastName', { required: true })}
label="Last Name"
error={errors.profile?.lastName?.message as string | undefined}
/>
<Button
value={true}
type="submit"
role="button"
disabled={isSubmitting || disabled || !isValid}
>
Submit
</Button>
</Stack>
</form>
</Container>
);
}
On the API side, I've created a function to upsert the client to MongoDB:
'use server';
/**
* @throws Will throw an error if the patch object is empty after parsing.
*/
export async function upsertClient(
id: string,
patch: Partial<Client>
): Promise<UpdateResult> {
const parsedPatch = deepPartialify(ClientUpsertSchema).parse(patch);
if (!parsedPatch || Object.keys(parsedPatch).length === 0) {
throw new Error('No patch data found');
}
const dotNotationPatch = getDotNotation(parsedPatch);
const collection = client.db('my-db').collection('clients');
const result = await collection.updateOne(
{ _id: id ? new ObjectId(id) : new ObjectId() },
{ $set: dotNotationPatch },
{ upsert: true }
);
revalidatePath(`/client/${id}`);
return result;
}
You may notice the dance I'm having to do with getDotNotation
before sending the patch object. More on that in this blog post. I'm also using a function to make all deeply nested values in my Zod schema optional with deepPartialify()
. More on that here.
For the most part, though, pretty straightforward server work. I'm validating the data on the server and then passing data to MongoDB. Because I'm using Next.js, I can add the 'use server';
directive so that I can call this function directly as a server action on the client.
Server Tests
Let's start writing tests for the API:
import {
upsertClient
} from '../client';
import { Db, MongoClient, ObjectId } from 'mongodb';
import { ObjectIdClient, Client } from '../schema';
declare global {
var __MONGO_URI__: string;
var __MONGO_DB_NAME__: string;
}
let mockClient: MongoClient;
let mockDb: Db;
beforeAll(async () => {
mockClient = await MongoClient.connect(global.__MONGO_URI__, {});
mockDb = mockClient.db(globalThis.__MONGO_DB_NAME__);
(client.db as any as jest.Mock) = jest.fn().mockReturnValue(mockDb);
await mockDb.collection('clients').insertMany([clientOne, clientTwo]);
});
describe('upsertClient', () => {
const patchId = '677578204c102d057aa44812';
test('should insert a new client with valid patch data', async () => {
const patch = { profile: { firstName: 'John', lastName: 'Doe' } };
const result = await upsertClient(
patchId,
patch as Partial<Client>
);
expect(result).toEqual({
acknowledged: true,
upsertedId: new ObjectId(patchId),
matchedCount: 0,
modifiedCount: 0,
upsertedCount: 1
});
const expectedPatchClient = {
_id: new ObjectId(patchId),
...patch
};
const patchedClient = await mockDb
.collection('clients')
.findOne({ _id: new ObjectId(patchId) });
expect(patchedClient).toEqual(expectedPatchClient);
});
test('should update a client with valid patch data', async () => {
const patch = { profile: { firstName: 'Jim' } };
const result = await upsertClient(
patchId,
patch as Partial<Client>
);
expect(result).toEqual({
acknowledged: true,
upsertedId: null,
matchedCount: 1,
modifiedCount: 1,
upsertedCount: 0
});
const expectedPatchClient = {
_id: new ObjectId(patchId),
profile: { firstName: 'Jim', lastName: 'Doe' }
};
const patchedClient = await mockDb
.collection('clients')
.findOne({ _id: new ObjectId(patchId) });
expect(patchedClient).toEqual(expectedPatchClient);
})
};
Lot's going on here, but I'll summarize. These two tests are validating the expected behavior:
Additionally, I'm making use of the jest-mongodb preset to spin up a local instance of mongodb. This allows me to have a test db available with every run of Jest. Very handy!
Once I've verified my tests are passing, I'm ready to move on to the client.
Client Tests
There are several tests I could write for my ClientUpsert
component. For demonstration, I'll focus on verifying a successful upsert:
/**
* @jest-environment jsdom
*/
import React from 'react';
import {
fireEvent,
screen,
waitFor
} from '@testing-library/react';
import { renderWithMantineProvider } from '@/lib/test-util/renderWithMantineProvider';
import { componentWithToastify } from '@/lib/test-util/renderWithToastify';
import '@testing-library/jest-dom';
jest.mock('@/lib/api/client/client', () => ({
__esModule: true,
upsertClient: jest.fn(),
}));
jest.mock('next/navigation', () => ({
__esModule: true,
}));
import ClientUpsert from './ClientUpsert';
import { upsertClient } from '@/lib/api/client/client';
describe('<ClientUpsert/ >', () => {
test('Should show successful update notification after client updated', async () => {
(upsertClient as jest.Mock).mockReturnValue({
acknowledged: true,
upsertedId: null,
matchedCount: 1,
modifiedCount: 1,
upsertedCount: 0
});
// Arrange
renderWithMantineProvider(
componentWithToastify(<ClientUpsert client={clientOne} />)
);
const firstName = screen.getByLabelText('First Name');
const submit = screen.getByRole('button');
// Act
fireEvent.change(firstName, {
target: { value: 'Big Tuna' }
});
// Wait for the form state to update
await waitFor(() => {
expect(submit).toBeEnabled();
});
fireEvent.click(submit);
// Assert
expect(await screen.findByText('Client Updated')).toBeInTheDocument();
});
});
Here I'm implmenting a three-phased test. I want this to be a true unit test of the component, so I'm going to mock the upsertClient
call. We'll handle seeing the two interact in my end-to-end test.
The nice thing about doing so is that, should the logic of this component be used elsewhere with another method passed to the onSubmit
, I don't have to worry about writing a whole new set of tests for it. We can simply focus on the UI performing as it should.
To stay true to the philosophy of React Testing Library, I'm also only concerned with seeing the visual sign of a successful upsert. In this case, it's a toast message appearing on the page.
With tests passing, it's time for end-to-end testing!
End-To-End Testing with Playwright
Playwright is my flavor of the month for this project, and it's been a great experience so far! I'll skip the many quality-of-life features and will focus on finishing out our task here.
Let's write our user flow:
import test, { expect } from '@playwright/test';
import { randomUUID } from 'node:crypto';
test.describe('New Client', () => {
const clientInput = {
firstName: 'Test',
lastName: randomUUID(),
};
const clientFullName = `${clientInput.firstName} ${clientInput.lastName}`;
test('should upsert a new client', async ({ page }) => {
await page.goto('./client/new');
expect(page.getByRole('heading', { name: 'New Client' })).toBeVisible();
await page.getByLabel('First Name').fill(clientInput.firstName);
await page.getByLabel('Last Name').fill(clientInput.lastName);
const submitButton = page.getByRole('button', { name: 'Submit' });
expect(submitButton).toBeEnabled();
submitButton.click();
await expect(
page.getByRole('heading', {
name: clientFullName
})
).toBeVisible();
await page.goto('./clients');
await expect(page.getByText(clientFullName)).toBeVisible();
});
});
Reading the code, the user journey is being described almost in plain english. I'm navigating to the page, filling out the form, submitting, and then verifying on a page containing all clients that the new document exists.
This can be tricky to maintain. Depending on how you chose to target elements, you may find yourself needing to update your selectors with any change. Targeting by element role is a safe approach because we are viewing essential page elements from the perspective of the user. And not just the typical mouse-in-hand user, but for screen reader users as well!
Evaluating Time & Testing Your Deliverable
If I were to average out the amount of time spent trying to overachieve as a tester, I would say I spent nearly half of my time developing and half writing the test suite.
In some projects, that might not be a problem. However, I can see where doing this yielded diminishing returns after a certain point. Hypothetically, this scenario of a single use page likely only needed an end-to-end test, perhaps with integration tests for my API endpoints that will likely see reuse.
Yet, in another project, if this took two devs, one on the front end, one on the back end, I would want both of them delivering tests for their portions.
At the end of the day, like any other choice in software, the answer to "what to test" is "it depends." Each application is different in scope, technology, and usage. A reusable widget in an iFrame can likely benefit more from unit testing, while your client sign up flow will need e2e testing. It's really dependent on the size of your deliverable, then.
Not to say that if you have e2e coverage, you can skip over integration and unit tests. It could be a headache breaking those components out as modular units if that's the case.
Ultimately, though, time is a limited resource. Testing itself is worth the investment of time for several reasons: confidence in the functionality of your app, thoughtfulness around edge cases, and communication of it's expected behavior. Where you chose to invest that time, however, is dependent on your highest priority outcome.
Supplemental Reading
Three Phase Tests
From Justin Weiss on how your tests should work in phases:
Lovely. Flexible enough to accomodate multiple levels of complexity, while giving enough of a framework to get you started on the path to testing quickly.
David Lynch Beyond Words
Kyle MacLachlan wrote a beautiful piece in memory of David Lynch for The New York Times:
Though my lifelong friend, collaborator and mentor David Lynch was as eloquent as anyone I’d ever met — and a brilliant writer — he was not necessarily a word person...
How could words possibly do justice to an experience like that?
It’s why David was not just a filmmaker: He was a painter, a musician, a sculptor and a visual artist — languageless mediums.
When you are outside language, you are in the realm of feeling, the unconscious, waves. That was David’s world. Because there’s room for other people — as the listeners, the audience, the other end of the line — to bring some of themselves.
To David, what you thought mattered, too.
Flow Over Skill
I'm dumbfounded by how there is no correlation between how skilled you are and how much enrichment you get out of practicing an art form.
I spent years in strict training to achieve a certain level of performance ability in music, under an assumption that fulfillment comes from mastery. And don't get me wrong — there is a certain freedom in being able to smoothly skate across an instrument.
But it's not a requirement. And the joy can come from day 1.
The real source is flow. There are plenty reasons to do creative work, but I'd say most of us find the magic in those moments where time slips away.
There are several ingredients to reaching that state. When it comes to ability, though, the key lever is how well the task in front of you matches your skill level.
Is it challenging enough where you're doing a bit of reaching?
Is it easy enough where you don't feel totally overwhelmed by what you're doing?
That's it. And it's relative.
Saxophone is my most fluent instrument. To achieve flow, I would have to be working on a new piece with significant technical and expressive challenges to get there. But when I'm playing guitar, I find it just by spending time moving between two chords again and again and again.
Especially as we're getting started, we're moved by the product of creative work. Someone's ability as a performer inspires us, or a stunning painting really grabs us. That's great for lighting the spark.
If that's the only source of fuel in the practice, though, there's this resentment of not being at that ability.
Most of the fulfillment has to come from the actual act of doing the thing. Thankfully, a certain level of skill is not required. Just flow.
Star Eyes Chord Melody
Star eyes, flashing eyes in which my hopes rise
Let me show you where my heart lies
Let me prove that it adores
That lovingness of yours
🤩
Enjoying this lovely arrangement with Helen O'Connell & Jimmy Dorsey.
Crystal Ocean
Repetition and Meditation
A big reason I still play instruments is for how genuinely soothing it is.
When learning a technique or a piece, it's unavoidable: There are some things you'll just have to play at least several hundred times. Beginners tend to groan at that, and can get discouraged. When you commit to learning an instrument, you're signing up for a lifetime of repetition.
But I've come to like it. Repetition becomes something like prayer. Something gentle to keep your mind centered around something while the subconscious takes care of cleanup duty.
Or, for the secular, it's akin to the benefits of walking throughout the day. If you're not musically inclined, walking gets you pretty similar benefits. Just one foot in front of the other again and again. Connecting with something beautiful — in this case, nature.
For me, though, I like having a little tune learned by the time I make it home.
Deeply Partial Zod Schemas
It's not uncommon to set up a schema with nested properties. In Zod, it could look something like this:
export const userSchema = z.object({
_id: z.string(),
profile: UserProfileSchema,
email: z.array(UserEmailSchema),
calculated: CalculatedUserSchema.optional(),
system: SystemUserSchema.optional()
});
const userProfileSchema = z.object({
firstName: z.string().min(2).trim(),
lastName: z.string().min(2).trim(),
image: z.string().optional()
});
// etc...
This is great for verifying a type has all the required properties. It gets tricky when I want to set all the values as optional, as I might in a $set
object for MongoDB.
Previous versions of Zod had a method deepPartial
that handled just that. It's since been deprecated, but no library replacement has been provided.
In the meantime, you can pull in the original logic into your project to implement the previous logic. It's handily available on this GitHub thread.
Writing For
From Jeremy Keith's What the world needs:
If we’re going to be hardnosed about this, then the world doesn’t need any more books. The world doesn’t need any more music. The world doesn’t need art. Heck, the world doesn’t need us at all.
So don’t publish for the world.
When I write something here on my website, I’m not thinking about the world reading it. That would be paralyzing...
I’m writing for myself. I write to figure out what I think. I also publish mostly for myself—a public archive for future me. But if what I publish just happens to connect with one other person, I’m glad.
Just came across this from earlier in March and forgot that I 1.) already read it and 2.) got great inspiration from it. It's an especially hard problem when worrying about being original on the entire internet. But, as Jeremy points out earlier in the post, you don't have to be original.
Smoother MongoDB ObjectID Handling in Web Applications
Documents in MongoDB automatically generate a unique key on the _id
field. The value is an ObjectId
, a byte data type that is very lightweight. (More details in the docs.)
When querying your DB, you'll be returned an ObjectID
data type. There are several scenarios in a web application where that can become cumbersome:
To navigate around it, you can project the value to a string any time you query the db.
Here's an initial example of querying a collection in an API method:
export async function getPeople(query = {}): Promise<Person[]> {
const client = await clientPromise;
const collection = client.db('your-db').collection('people');
const people = await collection
.find<People>(query)
.toArray();
return people;
}
To convert the _id
value, we'll create an aggregation step to make the conversion with the $toString
operator
const addFieldAggregation = {
$addFields: {
_id: { $toString: '$_id' },
}
};
Now, when querying from your application, you can convert the string value back to an ObjectId
to query the intended document. I'll just convert our query to an aggregation to accomplish this:
import { ObjectId } from 'mongodb';
export async function getPeople(query = {}): Promise<Person[]> {
const client = await clientPromise;
const collection = client.db('your-db').collection('people');
const person = (await collection
.aggregate([
{
$match: query,
},
addFieldAggregation
])
.toArray()) as Person[];
return person;
}
Voilà!
Pace Layers
I've just been introduced to the idea of Pace Layering via Chris Coyier:
...If you feel frustration at how quickly or slowly a particular technology moves, are you considering its place within the layers? Perhaps that speed is because it is part of a system that pressures it to be that way or it being that way is beneficial to the system as a whole.
Amazing to think how transferable it is to other domains. And to have acceptance of the fact that some things are meant to move quickly, others more slowly. The case for government vs commerce in Steward Brand's original case for this feels perpetually timely.
This compliments well with a bit of advice from Derek Sivers to focus on what doesn't change:
Instead, forget predicting, and focus on what doesn’t change. Just like we know there will be gravity, and water will be wet, we know some things stay the same.
People always love a memorable melody. You can’t know what instrumentation or production style will be in fashion. So focus on the craft of making great melodies...
Instead of predicting the future, focus your time and energy on the fundamentals. The unpredictable changes around them are just the details.
Moonlight In Vermont
Telegraph cables, they sing down the highway,
And travel each bend in the road~
No recording more magical than Louis and Ella.