How Cypress Component Testing changed our workflow (and how I came to work here)

Back to Cypress blog

Before I joined Cypress last year, the alpha version of Cypress Component Testing changed the way my team did front-end work at my previous job, allowing us to consolidate three tools into one. This was the experience that ultimately led to me joining the Component Testing team at Cypress, where I got to help move the feature out of alpha and towards a full release.

Since then, we’ve released Cypress 10 with support for testing React, Vue, Angular, and (soon!) Svelte components, and I’m coming up on my one-year anniversary as an engineer with the company. There’s no better time to pause for a moment and reflect on what drew me to Cypress Component Testing in the first place, as well as share some of what I see coming up in the future, as teams learn what makes Cypress different from the component testing solutions that came before.

How Cy Component Testing changed our workflow

At the start of 2021 I was leading the front-end team at a North American fleet management company. There was plenty of front-end work to go around: all kinds of map layers, schedule and driver management, vehicle assignments, and various telemetry from devices on vehicles had to be displayed. We wrote a lot of tests.

To start out, I’ll just say that onboarding new developers to the front-end codebase didn’t feel too great back then. It required explaining the three separate tools that would have to be touched to do any front-end work, which each had their own little quirks to be learned. This bothered me, because I like engineers to have the simplest possible workflow so they can focus on their actual tasks, not all this other stuff. For a long time I've wished front-end development had more consistent routines and practices for getting work done, and just overall made sense in a way that it doesn’t always right now.

In order to create or modify component in our application, a developer would use the following tools:

Storybook

You want to see what you're building, and having quick access to the different states through “knobs” and “actions” is a must. These help you make sure that when working on one state of the component, you haven't broken some other state, and allows you to review all the possibilities. So we needed to mount the component in Storybook, and set up the various toggles so state be modified when viewing the component.

Jest / Vue Test Utils

Component testing was required for fast feedback during development, and coverage of a component’s API. This meant mounting the component using Vue Test Utils and running the tests with Jest in a simulated browser. To do this required a somewhat different approach to mounting and modifying state than Storybook, and introduced some new quirks – like figuring out exactly when to wait for a framework-specific thing like `vm.nextTick()`, and what to do when the simulated DOM turned out not to have an API the components were expecting to use. No user would ever know or care about such things because they are specific to the test environment.

Cypress

Finally, once the component was complete, it could be situated in its proper place in the application, and an end-to-end test would be written. This test reached the component as a user would, and validated that it was working correctly. This required setting a test up to visit a page and perform user actions to get it into a certain state, then asserting the correct behavior and appearance. The addition of a real browser and Cypress’s built-in actionability checks meant it was worth repeating everything from the component tests in this new context, since this is how users interact with web applications.

These three tools all had a roughly similar starting point in this workflow – you mounted the component someplace, either on its own or in the application. Then things diverged, because each mounting situation was a little different, and the goals were different too. A developer needed to switch context many times in the course of working on a component, each time incurring a cost to their attention. It wasn’t always easy to decide whether to be in Cypress, Storybook, or Jest when making changes, or to just look at the application itself and figure out the tests and stories later.

Onboarding people to this was a bit of a drag. For a new developer wanting to get their first PR under their belt, it was a lot to take in, and the same things had to be done three times in slightly different ways.

Developers want to focus on making things and delivering value. We’ve learned to automate repeated tasks, and “near-repetition” that can’t be automated away feels frustrating, like there should be a better way.

We also had some problems maintaining everything over the long run that I'll describe below.


A component test running in Cypress with the Command Log on the left and the Component Under Test on the right.

Maintenance troubles with the old workflow

Every team is different and uses tools differently, so I don’t think these are universal problems as such, but I suspect they will at least be familiar to most readers. Here are some of the problems we experienced:

Storybook

Nobody would notice when existing stories broke, because only the development team used Storybook, and we mostly used it when creating new components, so that we could do isolated demos and discuss them with other stakeholders. Opening up the story for an existing component would often reveal that it didn’t render any more due to component API changes, or any number of other things that had been modified over time. The developer would have to decide whether to fix Storybook and then work on the component, or just drive development with the component rendering in the app, and run the existing tests.

Jest / Vue Test Utils

It turned out a lot of our component tests were difficult to work with. Some people are used to unit testing “the methods on a class” and had taken that approach to UI component testing, exercising each internal computed value and method of a Vue component to confirm it returned the right value. This made updating and refactoring components a bit of an ordeal, because we couldn’t just make internal changes and re-run the tests, the tests were so focused on the internals that we had to refactor both at once, dramatically reducing the value of the tests themselves, and introducing risk.

One reason for this is that, in a simulated browser, it’s often easier to do this kind of testing than to test components “as a user”. These unit tests don’t require interacting with the simulated browser and DOM, and working around the various limitations of doing so. But tests exist to help us change and maintain our code without breaking things we care about. To do that, we have to be able to run the same tests before and after refactoring, and the tests should only fail if things that impact users have broken. What matters is the what the component shows a user, not how it computes what to show the user.

Cypress

Because our Jest tests didn’t run in a real browser and weren’t always testing user-focused behavior, we didn’t tend to rely on them for confidence about our components’ functionality for users. This meant our Cypress end-to-end tests often took on a “component-like” focus, where the test would repeatedly visit a page, with slightly different scenarios, with the goal of checking if one small piece of the page looked a certain way. This gave us the coverage we needed, but felt like repetition, and these tests were pretty slow relative to what they were testing. Plus, left us wondering if we really needed the component tests in Jest after all. But Jest tests provide fast feedback, and they also cover props and events, and keeping the API stable is important for component-based development. So we kept them around.


A component test failing due to an unexpected character in the the username. Note that the second test in this spec is checking events fired by the user clicking "Log Out" and is unaffected by this.

Adoption of Cypress Component Testing

The alpha of Cypress Component Testing launched in April 2021, and I checked it out right away, because even though I had learned to get along with Vue Test Utils and Jest, I much preferred Cypress for the debugging experience, network stubbing, and APIs for events and assertions. I did not like hopping back and forth between two different testing setups. I asked the team if they’d like to explore this, and they said yes, so after some work to get it up and running (which I’ve written about here), we just … stopped writing new Jest tests. All new components were written using Cypress Component Testing, and it worked great.

Since we could see and debug the component right there in Cypress, there was no immediate need to set up a story for the component and see it in isolation that way. We still needed Storybook for component demos at company-wide sprint reviews, at first. But I realized pretty quickly that I could use Cypress, with cy.pause(), to play through a demo of all the states of our new components and explain what user actions resulted in each state. cy.pause() is a command that freezes the current test, and provides a "play" button to resume it, which can lead to another pause at a different state. Fantastic for demos, and totally inert in run mode if you accidentally commit it.

This was just as effective at putting across the information for the demo, and relied only on work we’d already done for testing reasons. It even helped some stakeholders better understand what UI tests are, how they help us prevent regressions that users would care about.

I do want to point out here that we had a very narrow use case for Storybook, in that it was entirely built around seeing components in development, and demonstrating those components for other stakeholders. We were able to stop using Storybook because of this narrow usage, and because we didn't deploy our Storybook for anybody else to depend on.

All of a sudden, having to bounce around between three tools was behind us and a new, streamlined development workflow emerged:

  1. Create an empty component and an empty component spec that mounts it
  2. Run the component spec in Cypress
  3. Write a test for your first state and start building our your component
  4. Watch your component in the browser as you develop and write the test
  5. If the component emits events, use spies as event handlers
  6. After creating and testing the first state, make a new block in the same file for the next state , mounting the component with different props
  7. Write the code and tests for that state
  8. Repeat until all required states have been rendered, and the correctness of the component in each state has been asserted

Combining component and end-to-end test coverage

Now the component has a single spec that documents and validates all of its possible states. The test uses the component’s API to mount, so props are tested, and it’s super easy to spy on events in a framework-agnostic way. It runs in a real browser, meaning no jumping through framework lifecycle hoops either. And it renders the component visually in a way that is easy to understand and debug.

Quite often this pattern helps components to be more isolated and makes us think twice before reaching for global state when it might be better to pass values in via props. But if we need global state or wrapper components, those things can be provided to the component as well.

All that’s missing at this point is the end-to-end test, which comes when the component is added to the app. But now that end-to-end test is free to focus more on the overall user journey and less on exhaustively testing the correctness of every component on the page. The end-to-end test wants to know if, given a certain set of conditions, the components are in the right state but they don’t have to repetitively do a deep check of the "correctness" of the state itself.

As an example of this, a component test might deeply check the accessibility of a checkout form, making sure it has the right labels and keyboard behavior. It would confirm that the form is usable and captures the user's order correctly given certain items in a cart object passed in as a prop.

The end-to-end tests would make sure the same form submits multiple orders correctly to the backend, at the end of various journeys through the application adding items to the real shopping cart, using discount codes etc. Each cycle through the flow would not need to repeat the deep checks done in the component test.

The component test can cover those things, because the only practical difference between component and end-to-end tests is: cy.mount(ExampleComponent) or cy.visit('https://example.com'). The same user-facing commands run in the same browser for both.

This is what was so appealing to me about Cypress Component Testing. Not just that I could see my components and run them in the real browser, but that my end-to-end and component tests were speaking the same language and using the same commands. Developers only had to learn one thing. I loved it. I still love it. It’s the best.

Here's a quick video of the workflow I enjoy so much:

Video discussing cy.pause(), using Cypress with your IDE, and the advantage of component testing providing fast feedback

Looking to the future

There are two reasons I’m so excited about what comes next when component and end-to-end tests are unified using Cypress.

QA + dev team collaboration

The first is that this could be and advantage for QA Engineers who currently use Cypress, in a way that has yet to be deeply explored. Here’s why:

  1. If you know how to read and write a Cypress End-to-End test, you know how to read and write all the relevant parts of a Cypress Component Test.  When a feature is “thrown over the wall” for test automation, it’s possible to understand exactly what user facing behavior is already covered in the component test (by reading it, or running it). You can use that information to guide what needs to be covered from the end-to-end test, since they run the same commands. You can even lift whole commands and element locators from the component tests if needed.
  2. Being able to update the user-facing part of a component test can increase participation by a QA Engineer in the design of tests during development, so that developers can get fast feedback about certain failures without waiting for CI.
  3. Developers can more easily contribute to end-to-end tests too, if they are not having to hop between two different mental models. Now the choice of what to test where becomes more about goals, strategy, and tradeoffs – not technical limitations or silos between teams.

All of the above works because the user-facing test code is in the same world across testing types, even if it’s written by different teams with different goals. There’s so much potential here and I can’t wait to see the kind of patterns people create around this. Big thanks to Filip Hric for talking through some of these ideas with me from the perspective of a tester!

Standardized workflow to help people learn

The second thing I’m really excited about is what a smooth on-ramp to testing and development Cypress Component Testing offers for people who are newer to the industry and its tools. Testing is a core part of professional software development, but it’s not taught alongside the main technical skills that go with building websites, like HTML, CSS and JavaScript, so beginners often come to it late, if at all, and it can seem complicated and intimidating. It’s a pet peeve of mine that more learning resources don’t explain standard professional practices like testing, or the many non-programming skills you need to break down a design and bring it to life with code.

Cypress Component Tests provide a perfect local dev server to render components people are creating as they learn development, and a way to learn about how front-end architecture and testing works at the same time. Being able to isolate components and understand their separate responsibilities is a huge help when starting out, since there can be so many things going on at once. I’m really looking forward to tutorial content that can leverage Cypress component tests early in the process of just teaching people about component-based development more generally, and then introduce end-to-end testing as the application grows. If this type of content doesn’t appear on its own, I’m looking forward to making it!

Takeaways, resources, and shoutouts

So, how did trying out this alpha feature end up with me working on the Cypress team? It was a surprisingly straight line. In order to use component testing in my old Vue + Vuetify codebase, with all of its constraints, I needed a little help from the Cypress Discord. This got me talking with Bart Ledoux and Jess Sachs, two of the main developers of Cypress Component Testing, and I learned shortly after that (from Jess tweeting about it) that the Component Testing team was hiring.

I'd been thinking about applying for a job at Cypress for years and wasn't sure where exactly I'd fit in, but component testing was right in my wheelhouse.

I started the formal interview process and started researching the company more. I was delighted to find out that Shawn Taylor, an old friend from my Code for Atlanta days, worked at Cypress now. I asked what they thought about it and if it would be a good fit for me. And I realized Lachlan Miller, maintainer of Vue Test Utils and creator of many teaching resources about Vue and Cypress was already here too, so that was neat, I already knew his work.

It’s been a great year working here, learning how component testing works from the inside, and contributing to the Cypress open source repo, including implementing parts of the new Cypress 10 UI and the component testing features themselves.

If there's a takeaway from this post, it's this: I’m hopeful that others will see some of the same benefits I did of using Cypress for both component and end-to-end tests, and that the addition of component testing can improve the daily quality of life for developers and QA teams using Cypress.

One of my favorite things about this, and one of our greatest responsibilities at Cypress, is that we make a tool you depend on for work, and what we do can make a big difference to how your day goes. I’m really happy that we are a part of so many developers’ daily process, and I see component testing as a huge step forward in streamlining the front-end workflow.

To get started with component testing, check out the docs for writing your first component test.