Cypress is an automated test runner for browser-based applications and pages. I’ve used it for years to write end-to-end tests for web projects, and was happy to see recently that individual component testing had come to Cypress. I work on a large enterprise Vue application, and we already use Cypress for end-to-end tests. Most of our unit and component tests are written with Jest and Vue Test Utils.
Once component testing arrived in Cypress, my team was all in favor of upgrading and trying it out. You can learn a lot about how component testing works directly from the Cypress docs, so I’m going skip over some of the setup steps and focus on what it is like to work with component tests — what do they look like, how are we using them, and some Vue-specific gotchas and helpers we found.
Disclosure! At the time I wrote the first draft of this article, I was the front-end team lead at a large fleet management company where we used Cypress for testing. Since the time of writing, I’ve started working at Cypress, where I get to contribute to the open source test runner.
All the examples mentioned here are valid at the time of writing using Cypress 8. It’s a new feature that’s still in alpha, and I wouldn’t be surprised if some of these details change in future updates.
If you already have a background in testing and component tests, you can skip right to our team’s experiences.
What a component test file looks like
For a simplified example, I’ve created a project that contains a “Privacy Policy” component. It has a title, body, and an acknowledgment button.
When the button is clicked, an event is emitted to let the parent component know that this has been acknowledged. Here it is deployed on Netlify.
Now here’s the general shape of a component test in Cypress that uses some of the feature’s we are going to talk about:
import { mount } from '@cypress/vue'; // import the vue-test-utils mount function
import PrivacyPolicyNotice from './PrivacyPolicyNotice.vue'; // import the component to test
describe('PrivacyPolicyNotice', () => {
it('renders the title', () => {
// mount the component by itself in the browser 🏗
mount(PrivacyPolicyNotice);
// assert some text is present in the correct heading level 🕵️
cy.contains('h1', 'Privacy Policy').should('be.visible');
});
it('emits a "confirm" event once when confirm button is clicked', () => {
// mount the component by itself in the browser 🏗
mount(PrivacyPolicyNotice);
// this time let's chain some commands together
cy.contains('button', '/^OK/') // find a button element starting with text 'OK' 🕵️
.click() // click the button 🤞
.vue() // use a custom command to go get the vue-test-utils wrapper 🧐
.then((wrapper) => {
// verify the component emitted a confirm event after the click 🤯
expect(wrapper.emitted('confirm')).to.have.length(1)
// `emitted` is a helper from vue-test-utils to simplify accessing
// events that have been emitted
});
});
});
This test makes some assertions about the user interface, and some about the developer interface (shoutout to Alex Reviere for expressing this division in the way that clicked for me). For the UI, we are targeting specific elements with their expected text content. For developers, we are testing what events are emitted. We are also implicitly testing that the component is a correctly formed Vue component; otherwise it would not mount successfully and all the other steps would fail. And by asserting specific kinds of elements for specific purposes, we are testing the accessibility of the component — if that accessible button
ever becomes a non-focusable div
, we’ll know about it.
Here’s how our test looks when I swap out the button for a div
. This helps us maintain the expected keyboard behavior and assistive technology hints that come for free with a button element by letting us know if we accidentally swap it out:
A little groundwork
Now that we’ve seen what a component test looks like, let’s back up a little bit and talk about how this fits in to our overall testing strategy. There are many definitions for these things, so real quick, for me, in our codebase:
- Unit tests confirm single functions behave as expected when used by a developer.
- Component tests mount single UI components in isolation and confirm they behave as expected when used by an end-user and a developer.
- End-to-end tests visit the application and perform actions and confirm the app as whole behaves correctly when used by an end-user only.
Finally, integration testing is a little more of a squishy term for me and can happen at any level — a unit that imports other functions, a component that imports other components, or indeed, an “end-to-end” test that mocks API responses and doesn’t reach the database, might all be considered integration tests. They test more than one part of an application working together, but not the entire thing. I’m not sure about the real usefulness of that as a category, since it seems very broad, but different people and organizations use these terms in other ways, so I wanted to touch on it.
For a longer overview of the different kinds of testing and how they relate to front-end work, you can check out “Front-End Testing is For Everyone” by Evgeny Klimenchenko.
Component tests
In the definitions above, the different testing layers are defined by who will be using a piece of code and what the contract is with that person. So as a developer, a function that formats the time should always return the correct result when I provide it a valid Date object, and should throw clear errors if I provide it something different as well. These are things we can test by calling the function on its own and verifying it responds correctly to various conditions, independent of any UI. The “developer interface” (or API) of a function is all about code talking to other code.
Now, let’s zoom in on component tests. The “contract” of a component is really two contracts:
- To the developer using a component, the component is behaving correctly if the expected events are emitted based on user input or other activity. It’s also fair to include things like prop types and validation rules in our idea of “correct developer-facing behavior,” though those things can also be tested at a unit level. What I really want from a component test as a developer is to know it mounts, and sends the signals it is supposed to based on interactions.
- To the user interacting with a component, it is behaving correctly if the UI reflects the state of the component at all times. This includes more than just the visual aspect. The HTML generated by the component is the foundation for its accessibility tree, and the accessibility tree provides the API for tools like screen readers to announce the content correctly, so for me the component is not “behaving correctly” if it does not render the correct HTML for the contents.
At this point it’s clear that component testing requires two kinds of assertions — sometimes we check Vue-specific things, like “how many events got emitted of a certain type?”, and sometimes we check user-facing things, like “did a visible success message actually end up on the screen though?”
It also feels like component level tests are a powerful documentation tool. The tests should assert all the critical features of a component — the defined behaviors that are depended on — and ignore details that aren’t critical. This means we can look to the tests to understand (or remember, six months or a year from now!) what a component’s expected behavior is. And, all going well, we can change any feature that’s not explicitly asserted by the test without having to rewrite the test. Design changes, animation changes, improving the DOM, all should be possible, and if a test does fail, it will be for a reason you care about, not because an element got moved from one part of the screen to another.
This last part takes some care when designing tests, and most especially, when choosing selectors for elements to interact with, so we’ll return to this topic later.
How Vue component tests work with and without Cypress
At a high level, a combination of Jest and the Vue Test Utils library has becomes more or less the standard approach to running component tests that I’ve seen out there.
Vue Test Utils gives us helpers to mount a component, give it its options, and mock out various things a component might depend on to run properly. It also provides a wrapper
object around the mounted component to make it a little easier to make assertions about what’s going on with the component.
Jest is a great test runner and will stand up the mounted component using jsdom
to simulate a browser environment.
Cypress’ component test runner itself uses Vue Test Utils to mount Vue components, so the main difference between the two approaches is context. Cypress already runs end-to-end tests in a browser, and component tests work the same way. This means we can see our tests run, pause them mid-test, interact with the app or inspect things that happened earlier in the run, and know that browser APIs that our application depends on are genuine browser behavior rather than the jsdom
mocked versions of those same features.
Once the component is mounted, all the usual Cypress things that we have been doing in end-to-end tests apply, and a few pain points around selecting elements go away. Mainly, Cypress is going to handle simulating all the user interactions, and making assertions about the application’s response to those interactions. This covers the user-facing part of the component’s contract completely, but what about the developer-facing stuff, like events, props, and everything else? This is where Vue Test Utils comes back in. Within Cypress, we can access the wrapper that Vue Test Utils creates around the mounted component, and make assertions about it.
What I like about this is that we end up with Cypress and Vue Test Utils both being used for what they are really good at. We can test the component’s behavior as a user with no framework-specific code at all, and only dig into Vue Test Utils for mounting the component and checking specific framework behavior when we choose to. We’ll never have to await
a Vue-specific $nextTick
after doing some Vue-specific thing to update the state of a component. That was always the trickiest thing to explain to new developers on the team without Vue experience — when and why they would need to await
things when writing a test for a Vue component.
Our experience of component testing
The advantages of component testing sounded great to us, but of course, in a large project very few things can be seamless out of the box, and as we got started with our tests, we ran into some issues. We run a large enterprise SPA built using Vue 2 and the Vuetify component library. Most of our work heavily uses Vuetify’s built-in components and styles. So, while the “test components by themselves” approach sounds nice, a big lesson learned was that we needed to set up some context for our components to be mounted in, and we needed to get Vuetify and some global styles happening as well, or nothing was going to work.
Cypress has a Discord where people can ask for help, and when I got stuck I asked questions there. Folks from the community —as well as Cypress team members — kindly directed me to example repos, code snippets, and ideas for solving our problems. Here’s a list of the little things we needed to understand in order to get our components to mount correctly, errors we encountered, and whatever else stands out as interesting or helpful:
- Importing and using Vuetify
- Adding some attributes that Vuetify depends on to the
__cy_root
element - Using
.spec
files placed along side component files, not in a separate test folder - Dealing with a custom command import conflict between component tests and end-to-end tests
- Accessing the Vue wrapper easily in a Cypress context
Importing Vuetify
Through lurking in the Cypress Discord, I’d seen this example component test Vuetify repo by Bart Ledoux, so that was my starting point. That repo organizes the code into a fairly common pattern that includes a plugins
folder, where a plugin exports an instance of Veutify. This is imported by the application itself, but it can also be imported by our test setup, and used when mounting the component being tested. In the repo a command is added to Cypress that will replace the default mount
function with one that mounts a component with Vuetify.
Here is all the code needed to make that happen, assuming we did everything in commands.js
and didn’t import anything from the plugins
folder. We’re doing this with a custom command which means that instead of calling the Vue Test Utils mount
function directly in our tests, we’ll actually call our own cy.mount
command:
// the Cypress mount function, which wraps the vue-test-utils mount function
import { mount } from "@cypress/vue";
import Vue from 'vue';
import Vuetify from 'vuetify/lib/framework';
Vue.use(Vuetify);
// add a new command with the name "mount" to run the Vue Test Utils
// mount and add Vuetify
Cypress.Commands.add("mount", (MountedComponent, options) => {
return mount(MountedComponent, {
vuetify: new Vuetify({});, // the new Vuetify instance
...options, // To override/add Vue options for specific tests
});
});
Now we will always have Vuetify along with our components when mounted, and we can still pass in all the other options we need to for that component itself. But we don’t need to manually add Veutify each time.
Adding attributes required by Vuetify
The only problem with the new mount
command above is that, to work correctly, Vuetify components expect to be rendered in a certain DOM context. Apps using Vuetify wrap everything in a <v-app>
component that represents the root element of the application. There are a couple of ways to handle this but the simplest is to add some setup to our command itself before it mounts a component.
Cypress.Commands.add("mount", (MountedComponent, options) => {
// get the element that our mounted component will be injected into
const root = document.getElementById("__cy_root");
// add the v-application class that allows Vuetify styles to work
if (!root.classList.contains("v-application")) {
root.classList.add("v-application");
}
// add the data-attribute — Vuetify selector used for popup elements to attach to the DOM
root.setAttribute('data-app', 'true');
return mount(MountedComponent, {
vuetify: new Vuetify({}),
...options,
});
});
This takes advantage of the fact that Cypress itself has to create some root element to actually mount our component to. That root element is the parent of our component, and it has the ID __cy_root
. This gives us a place to easily add the correct classes and attributes that Vuetify expects to find. Now components that use Vuetify components will look and behave correctly.
One other thing we noticed after some testing is that the required class of v-application
has a display
property of flex
. This makes sense in a full app context using Vuetify’s container system, but had some unwanted visual side effects for us when mounting single components — so we added one more line to override that style before mounting the component:
root.setAttribute('style', 'display: block');
This cleared up the occasional layout issues and then we were truly done tweaking the surrounding context for mounting components.
Getting spec files where we want them
A lot of the examples out there show a cypress.json
config file like this one for component testing:
{
"fixturesFolder": false,
"componentFolder": "src/components",
"testFiles": "**/*.spec.js"
}
That is actually pretty close to what we want since the testFiles
property accepts a glob pattern. This one says, Look in any folder for files ending in
In our case, and probably many others, the project’s .spec.js
.node_modules
folder contained some irrelevant spec.js
files that we excluded by prefixing !(node_modules)
like this:
"testFiles": "!(node_modules)**/*.spec.js"
Before settling on this solution, when experimenting, we had set this to a specific folder where component tests would live, not a glob pattern that could match them anywhere. Our tests live right alongside our components, so that could have been fine, but we actually have two independent components
folders as we package up and publish a small part of our app to be used in other projects at the company. Having made that change early, I admit I sure did forget it had been a glob to start with and was starting to get off course before popping into the Discord, where I got a reminder and figured it out. Having a place to quickly check if something is the right approach was helpful many times.
Command file conflict
Following the pattern outlined above to get Vuetify working with our component tests produced a problem. We had piled all this stuff together in the same commands.js
file that we used for regular end-to-end tests. So while we got a couple of component tests running, our end-to-end tests didn’t even start. There was an early error from one of the imports that was only needed for component testing.
I was recommended a couple of solutions but on the day, I chose to just extract the mounting command and its dependencies into its own file, and imported it only where needed in the component tests themselves. Since this was the only source of any problem running both sets of tests, it was a clean way to take that out of the the end-to-end context, and it works just fine as a standalone function. If we have other issues, or next time we are doing cleanup, we would probably follow the main recommendation given, to have two separate command files and share the common pieces between them.
Accessing the Vue Test Utils wrapper
In the context of a component test, the Vue Test Utils wrapper is available under Cypress.vueWrapper
. When accessing this to make assertions, it helps to use cy.wrap
to make the result chain-able like other commands accessed via cy
. Jessica Sachs adds a short command in her example repo to do this. So, once again inside commands,js
, I added the following:
Cypress.Commands.add('vue', () => {
return cy.wrap(Cypress.vueWrapper);
});
This can be used in a test, like this:
mount(SomeComponent)
.contains('button', 'Do the thing once')
.click()
.should('be.disabled')
.vue()
.then((wrapper) => {
// the Vue Test Utils `wrapper` has an API specifically setup for testing:
// https://vue-test-utils.vuejs.org/api/wrapper/#properties
expect(wrapper.emitted('the-thing')).to.have.length(1);
});
This starts to read very naturally to me and clearly splits up when we are working with the UI compared to when we are inspecting details revealed through the Vue Test Utils wrapper. It also emphasizes that, like lots of Cypress, to get the most out of it, it’s important to understand the tools it leverages, not just Cypress itself. Cypress wraps Mocha, Chai, and various other libraries. In this case, it’s useful to understand that Vue Test Utils is a third-party open source solution with its own entire set of documentation, and that inside the then
callback above, we are in Vue Test Utils Land — not Cypress Land — so that we go to the right place for help and documentation.
Challenges
Since this has been a recent exploration, we have not added the Cypress component tests to our CI/CD pipelines yet. Failures will not block a pull request, and we haven’t looked at adding the reporting for these tests. I don’t expect any surprises there, but it’s worth mentioning that we haven’t completed integrating these into our whole workflow. I can’t speak to it specifically.
It’s also relatively early days for the component test runner and there are a few hiccups. At first, it seemed like every second test run would show a linter error and need to be manually refreshed. I didn’t get to the bottom of that, and then it fixed itself (or was fixed by a newer Cypress release). I’d expect a new tool to have potential issues like this.
One other stumbling block about component testing in general is that, depending on how your component works, it can be difficult to mount it without a lot of work mocking other parts of your system. If the component interacts with multiple Vuex modules or uses API calls to fetch its own data, you need to simulate all of that when you mount the component. Where end-to-end tests are almost absurdly easy to get up and running on any project that runs in the browser, component tests on existing components are a lot more sensitive to your component design.
This is true of anything that mounts components in isolation, like Storybook and Jest, which we’ve also used. It’s often when you attempt to mount components in isolation that you realize just how many dependencies your components actually have, and it can seem like a lot of effort is needed just to provide the right context for mounting them. This nudges us towards better component design in the long run, with components that are easier to test and while touching fewer parts of the codebase.
For this reason, I’d suggest if you haven’t already got component tests, and so aren’t sure what you need to mock in order to mount your component, choose your first component tests carefully, to limit the number of factors you have to get right before you can see the component in the test runner. Pick a small, presentational component that renders content provided through props or slots, to see it a component test in action before getting into the weeds on dependencies.
Benefits
The component test runner has worked out well for our team. We already have extensive end-to-end tests in Cypress, so the team is familiar with how to spin up new tests and write user interactions. And we have been using Vue Test Utils for individual component testing as well. So there was not actually too much new to learn here. The initial setup issues could have been frustrating, but there are plenty of friendly people out there who can help work through issues, so I’m glad I used the “asking for help” superpower.
I would say there are two main benefits that we’ve found. One is the consistent approach to the test code itself between levels of testing. This helps because there’s no longer a mental shift to think about subtle differences between Jest and Cypress interactions, browser DOM vs jsdom
and similar issues.
The other is being able to develop components in isolation and getting visual feedback as we go. By setting up all the variations of a component for development purposes, we get the outline of the UI test ready, and maybe a few assertions too. It feels like we get more value out of the testing process up front, so it’s less like a bolted-on task at the end of a ticket.
This process is not quite test-driven development for us, though we can drift into that, but it’s often “demo-driven” in that we want to showcase the states of a new piece of UI, and Cypress is a pretty good way to do that, using cy.pause()
to freeze a running test after specific interactions and talk about the state of the component. Developing with this in mind, knowing that we will use the tests to walk through the components features in a demo, helps organize the tests in a meaningful way and encourages us to cover all the scenarios we can think of at development time, rather than after.
Conclusion
The mental model for what exactly Cypress as whole does was tricky for me to when I first learned about it, because it wraps so many other open source tools in the testing ecosystem. You can get up and running quickly with Cypress without having a deep knowledge of what other tools are being leveraged under the hood.
This meant that when things went wrong, I remember not being sure which layer I should think about — was something not working because of a Mocha thing? A Chai issue? A bad jQuery selector in my test code? Incorrect use of a Sinon spy? At a certain point, I needed to step back and learn about those individual puzzle pieces and what exact roles they were playing in my tests.
This is still the case with component testing, and now there is an extra layer: framework-specific libraries to mount and test components. In some ways, this is more overhead and more to learn. On the other hand, Cypress integrates these tools in a coherent way and manages their setup so we can avoid a whole unrelated testing setup just for component tests. For us, we already wanted to mount components independently for testing with Jest, and for use in Storybook, so we figured out a lot of the necessary mocking ideas ahead of time, and tended to favor well-separated components with simple props/events based interfaces for that reason.
On balance, we like working with the test runner, and I feel like I’m seeing more tests (and more readable test code!) showing up in pull requests that I review, so to me that’s a sign that we’ve moved in a good direction.
The post Testing Vue Components With Cypress appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.
source https://css-tricks.com/testing-vue-components-with-cypress/
No comments:
Post a Comment