Skip to content

Instantly share code, notes, and snippets.

@lmiller1990
Created August 10, 2023 06:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lmiller1990/41ae2ff782e2ffa7e516545a229ef546 to your computer and use it in GitHub Desktop.
Save lmiller1990/41ae2ff782e2ffa7e516545a229ef546 to your computer and use it in GitHub Desktop.
Integrate with the Stack

Component Testing Toolchain Mess

So for the last while (~10 years?) component tooling has been in a weird state, where it kind of "bolts on" to your app. Except once you start to do anything remotely complex (eg: in the real world, where you really want tools like Storybook, Jest, Cypress CT, whatever) things go off the rails, since "we integrate with your dev server and tooling" never seems to "just work".

Every year, a new layer of misdirection is added:

  • 2015: webpack, importing non JS assets (like import './style.css')
  • 2016: JSX, .vue file, babel, now we can live in the future RIGHT NOW
  • 2017: TypeScript, and of course, TSX
  • 2018: Jest gains popularity. It does NOT work with webpack, now you need *-jest-preprocessor
  • 2019: ESM actually works, so now we have type: module and .mjs and mts and all the rest. Except it doesn't work with Jest. Also, we what about SSR, Next.js, whatever
  • 2020: GraphQL, except it doesn't work with TypeScript, now you need like 500 preprocessors to be type safe.
  • 2021: Vite, ESM (except it's not really ESM, it works with CommonJS too, except when it doesn't)
  • 2022: It's so complex that "meta" frameworks are the norm, since it's too complex to acutally configure this stuff yourself, no single human can understand an entire toolchain anymore

The Problem

Every step of the way, component testing tooling has had to play catch up. You need to make sure your tool deals with all the formats - languages - bundlers - environments (client? server? isomorphic?) etc.

The problem is component, and testing tooling in general, is trying to pull the ecosystem towards it. We want to injest and deal with your compiler toolchain. Problem is, gravity is against us. Component tooling is the moon and the meta framework / production toolchain is the earth, we are just endlessly orbiting around, our field of gravity making little to no difference relative to the rest of the ecosystem.

So developers just don't bother testing a lot of the time, just some E2E and manual testing, and ship it to production. Maybe some E2E tests and manual QA. Is your thing accessible? Who knows, wait for a fine or pay Deque a bucket of money to find out.

The Solution

We should embed ourselves inside the actual application. Here's what I want. Imagine you have some complex page deep in your Next.js app. You want to test the 10 difference states of some specific component, what are you gonna do?

  1. Wrangle with Storybook or Cypress to get the component to render in isolation, if you don't give up because your toolchain is too complex to integrate.
  2. Write a lot of E2E tests (those are expensive and tedious to write, if you even have it set up)
  3. Use hot reload
  4. Just manually test a few states, ship it to production and hope for the best
export default function SomeDeepPage (props) {
  <Wrapper>
      <Wrapper2>
          <Wrapper3>
            <ComponentIWantToTest foo="bar" />
          </Wrapper3>
      <Wrapper2>
    <Wrapper>
}

So - you want to isolate and develop <ComponentIWantToTest>? To do so, you need to make sure it has all those wrappers, and they are all configured in the same fashion they are in production. So, right now you do (either with Storybook or Cypress, or Jest, or ANY TOOL):

  1. Re-create the wrappers as best you can, stub like 10000 things, probably you mess some up, now your test isn't really reflecting the real world
  2. Just test it in your actual app using hot reload or refreshing and navigating to the same page a bunch of times

(2) is what most people are doing, I'd say. They'll write some E2E tests after the fact (maybe). So - what we should do is capture the component in it's actual production state. Capture it in it's natural environment. Someone already did the work to set up the toolchain - like the Vercel / Next.js team - and a bunch of other engineers spend a ton of time building the app out.

The Dream

export default function SomeDeepPage (props) {
  <Wrapper>
      <Wrapper2>
          <Wrapper3>
+             <CaptureComponent>
+                 <ComponentIWantToTest foo="bar" />
+             <CaptureComponent>
          </Wrapper3>
      <Wrapper2>
   <Wrapper>
}

The <CaptureComponent> should just grab {children} and serialize the DOM. Then you slurp it up in whatever you want (some lightweight local server that serves it up, or something) and developers can play around, do visual testing, or whatever. It'll be stripped out during your production build. It's only for development / testing.

You could customize it, too, if you want:

  <Wrapper>
      <Wrapper2>
          <Wrapper3>
             <CaptureComponent testStates={{ foo: ['bar', 'qux'] }}>
                 <ComponentIWantToTest />
             <CaptureComponent>
          </Wrapper3>
      <Wrapper2>
   <Wrapper>

Now when <CaptureComponent> grabs {children}, it might do so and inject all the prop combinations. Maybe you don't even write a test, you just click through all the states, and grab all the variations, then every time you click through, or do local development, or run an E2E tests, <CaptureComponent> captures the content and diffs it against your existing snapshots. No need to "write" comopnents tests, you just drive them during your usual development workflow.

<CaptureComponent> could be tool agnostic - have some predefined format, then any tool could slurp it up and do things with it (Percy, Cypress, Storybook, whatever you want).

Instead of writing your tests alongside your component files, just embed it right in your app - which is already working with your toolchain. If your toolchain works, your component tests work. If it doesn't, that's got nothing to with your testing infrastruture. Maybe we don't even need CT infrastructure anymore - no more dev server integrations.

Trade Offs

There are trade offs here.

No Stub, No Spy

It's all real - no stub, no spy, no fake data. Is this even a problem? We all know the dangers of mocking/stubbing - less production like tests. You won't need it here, anyway - your app already does what it's supposed to do.

No Component Driver / TDD

You cannot drive development with components. You need the actual feature in some form (you could use fake data). This is a real trade off - you can't have frontend / component developers working in isolation anymore.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment