Juri Strumpflohner is a passionate software developer, most recently focusing on the JavaScript ecosystem. He’s a Google Developers Expert in Web Technologies, regularly speaks at conferences, meetups, teaches at Egghead.io or writes articles such as this one. Currently Juri works as a JavaScript Architect at Nrwl where he helps clients build high-quality software with monorepos and Nx. You cand find him on Twitter or his personal blog.
It all started with sprinkling some jQuery here and there to make our server-side rendered pages more dynamic and appealing. Since then we’ve come a long way. Nowadays, entire platforms are being built on the frontend, with JavaScript/TypeScript and your framework of choice. Those are no more only simple web pages, but rather sophisticated, feature-rich applications built for the browser.
As a result, we need to have a different approach to developing such software. Also on the frontend we need to think about topics such as application architecture, state management, modularisation, scaling development across multiple teams and most importantly, automation and quality assurance.
Nrwl’s Nx has been my preferred choice over the last couple of years when it comes to tackling such projects. Nx is a set of extensible dev tools for monorepos. While monorepos have their origin in large enterprises such as Google or Facebook, they’ve recently become more and more popular also for smaller projects. Why? Increased team velocity and less code/version management overhead. By having all the relevant code co-located in one git repository, it is easy to make project-wide refactoring, implement cross-project features, which otherwise would be much more tedious and time consuming. Monorepos also come with a cost though which is why you need great tooling to support you! And this is where Nx and Cypress come into play.
React App with Cypress tests in under a minute
** = npm installs excluded 😉
Nx supports Angular, React and Node out of the box and is potentially open to other frameworks via its plugin system. You can even have multiple different types of projects in the same workspace. But for now, going forward we’ll use use React as an example.
To get started let’s create a new workspace:$ npx create-nx-workspace mynxworkspace
After the workspace has been initialized, you’ll see a series preconfigured setups to choose from. We’ll choose React for this article:
The wizard continues asking for a couple of configs and workspace settings, like the app name to be generated, the styling framework to use etc. After that we should get the following Nx workspace:
Nx workspaces are structured into two main categories: apps & libs. As you can see we have the myfirstreactapp
generated in the apps
folder, while the libs
folder is still empty. Notice the myfirstreactapp-e2e
. That’s a fully functional Cypress setup to test our myfirstreactapp
.
Let’s launch the app with$ nx run myfirstreactapp:serve
or simply$ npm start
as myfirstreactapp
is the default project.
If we open the myfirstreactapp-e2e
folder, we se a fully functional Cypress setup with a pre-generated app.spec.ts
Cypress test.
These Cypress tests can simply be executed with$ nx run myfirstreactapp-e2e:e2e
To run them in watch mode, simply append --watch
to it and you’ll get the Cypress test runner we all learned to love ☺️
Cypress Code completion thanks to TypeScript
Nx loves TypeScript! Thus, all projects and Cypress tests are generated and preconfigured to use TypeScript. No more guessing, but rather code completion for Cypress commands.
Sharing Cypress Commands across apps & libs
If you haven’t checked out the Cypress Best Practices page, you definitely should. It is the first thing I suggest people to go read. Especially when it comes to selecting elements, which - if done wrongly - can lead to very fragile tests.
Hence, rather than writing a selector like..cy.get('h1').contains('Welcome to myfirstreactapp!');
..I add a data-cy
selector on the element I’d like to test. So in my app.tsx
component, let’s add data-cy="page-title"
In our app.spec.ts
we can then use the following selector:cy.get('[data-cy="page-title"]').contains('Welcome to myfirstreactapp!');
Always writing the entire ..get('[data-cy…
selector is repetitive, might be error prone and tedious. A perfect case for making it become a custom Cypress command. Normally you would simply place them in Cypress’s support/commands.ts
file but since an Nx workspace might potentially host multiple apps and libraries and thus have multiple Cypress based setups as well, I definitely want to share these Cypress commands among these.
That’s where Nx libs come into play. Libs are where most of the work happens. It is where you implement the domain/business features and import them into one or even multiple apps. Let’s create a library called e2e-utils
and place it under a shared
folder.
$ nx generate @nrwl/workspace:library --name=e2e-utils --directory=shared
We generate a @nrwl/workspace
library, which is a plain TypeScript library since we won’t need any React specific things in there. Note, you don’t have to know all these commands by heart. If you happen to use Visual Studio Code, you can install NxConsole which provides a nice UI driven approach for generating new libraries.
In the newly generated libs/shared/e2e-utils
library, we create a new folder commands
and an according index.ts
inside it. We use that file to host our custom Cypress commands that should be shared with the entire workspace.
Copy the following into your commands/index.ts
file:
/// <reference types="Cypress" />
declare namespace Cypress {
interface Chainable<Subject = any> {
getEl<E extends Node = HTMLElement>(
identifier: string
): Chainable<JQuery<E>>;
}
}
Cypress.Commands.add(
'getEl',
{ prevSubject: 'optional' },
(subject: Cypress.Chainable, identifier: string) => {
if (subject) {
return subject.find(`[data-cy="${identifier}"]`);
} else {
return cy.get(`[data-cy="${identifier}"]`);
}
}
);
As you can see, we extend the cy
object with a new function getEl
that automatically uses the data-cy
attribute.Let’s also export the file from our library, by adding the following to the libs/shared/e2e-utils/src/index.ts
:
import './lib/commands';
At this point we’re able to import it in our e2e tests for the myfirstreactapp
app. Open myfirstreactapp-e2e/src/support/index.ts
and import it accordingly:
Note, Nx uses path mappings (in thetsconfig.json
) to map our library exports to a proper scoped name@mynxworkspace/shared/e2e-utils
. That would potentially even allow us to publish our library and import it via NPM.
Finally we can refactor our app.spec.ts
to use the new cy.getEl(…)
function:
cy.getEl('page-title').contains('Welcome to myfirstreactapp!');
// cy.get('[data-cy="page-title"]').contains('Welcome to myfirstreactapp!');
With this setup, it is easy to place shareable commands into the e2e-utils
library and they’ll be ready to be used across the various Cypress setups in your workspace.
Cypress based component tests with Storybook
I love to use Storybook when creating shared UI components. It gives developers an easy way to visually test their components and fellow team members to check out what’s available. In an Nx workspace this makes even more sense because you might potentially have multiple teams working on it.
Storybook allows us to develop a component in isolation and provides an excellent documentation for UI components. Wouldn’t it be cool to also automatically test those Storybook’s with Cypress? Luckily Nx has got your back here as well.
To get started, let’s generate a React component library:$ nx generate @nrwl/react:library --name=greeter --directory=shared --style=scss
This should generate a new React library under shared/greeter
:
The component - intentionally - is super simple:
import React from 'react';
import './shared-greeter.scss';
export interface SharedGreeterProps {
name: string;
}
export const SharedGreeter = (props: SharedGreeterProps) => {
return (
<div>
<h1>Hi there, {props.name}</h1>
</div>
);
};
export default SharedGreeter;
As the next step, let’s add Storybook support, first of all, installing Nrwl’s Storybook dependency:$ npm i @nrwl/storybook --save-dev
Next we can again use one of the Nx code generators (called schematics) to generate the storybook configuration for our greeter
component library:$ nx generate @nrwl/react:storybook-configuration --name=shared-greeter --configureCypress
Note the --configureCypress
! The above command generates the storybook configuration for our greeter library, as well as a shared-greeter-e2e
Cypress setup
Before running the Storybook we have to add a story for our greeter component. At the same level as our shared-greeter.tsx
create a new file shared-greeter.stories.tsx
with the following content:
import { text } from '@storybook/addon-knobs';
import React from 'react';
import { SharedGreeter, SharedGreeterProps } from './shared-greeter';
export default {
component: SharedGreeter,
title: 'Shared Greeter'
};
export const primary = () => {
const sharedGreeterProps: SharedGreeterProps = {
personName: text('Person Name', 'Juri')
};
return <SharedGreeter personName={sharedGreeterProps.personName} />;
};
Then we can run it with:$ nx run shared-greeter:storybook
There’s one interesting property of Storybook. You can navigate to /iframe.html
and and control it via the URL. In our case, the story id would be shared-greeter--primary
and we can control the “Person Name” via the knob-Person Name query param.
For example:/iframe.html?id=shared-greeter--primary&knob-Person Name=Juri
We can leverage this knowledge in our Cypress tests! By having provided --configureCypress
when adding the Storybook configuration to our libary, Nx has automatically generated a Cypress setup for it. Open the apps/shared-greeter-e2e
project and create a new test greeter.spec.ts
inside the integration
folder (create it if it isn’t there).
describe('greeter component', () => {
it('should display greeting message', () => {
cy.visit('/iframe.html?id=shared-greeter--primary&knob-Person Name=Juri');
cy.getEl('greeting').contains('Hi there, Juri!');
});
it('should display the person name properly', () => {
cy.visit('/iframe.html?id=shared-greeter--primary&knob-Person Name=John');
cy.getEl('greeting').contains('Hi there, John!');
});
});
Note, following best practices, similarly as in the previous section, we add thedata-cy="greeting"
onto ourSharedGreeter
component and import our sharede2e-utils
Cypress commands.
From within the Cypress test we exercise our Story with different inputs and see whether our component reacts properly.
We can run the tests in the same way we did for the app previously, but now obviously passing our library project (feel free to pass --watch
as a param):$ nx run shared-greeter-e2e:e2e
Also, check out Isaac Mann’s talk about “E2E Testing at Half the Cost” which talks about Cypress and Storybook integration.
Running on CI
Automated tests are only useful if you can run them in an automated fashion on your CI server. Cypress has already an in-depth guide on Continuous Integration which is especially helpful to configure your CI environment to be able to run Cypress tests. Nx is fully optimized to be able to run in CI environments as well. As such it comes with a series of so-called “affected” commands. Internally Nx builds a graph of the workspace apps and libraries. You can generate it by running npm run dep-graph
. Right now the graph looks as follows:
Let’s create another react app and import the SharedGreeter
component. The graph changes to the following:
We also get a Cypress test setup for our 2nd react app, which also happens to import our greeter component. In a normal workspace, CI would run all of the tests. Clearly as our app grows (in particular in a monorepo scenario) this is not scalable. Nx however, is able to use this graph to calculate the libraries that have been touched and thus only run the necessary tests. Assume someone creates a PR, changing the SharedGreeter component.
In such scenario, running$ npm run affected:e2e
...would only execute the Cypress tests for our GreeterComponent as well as of my2ndreactapp
as they might both potentially be affected by the change. Running npm run affected:dep-graph
visualizes this:
This greatly improves the running time and helps to avoid unnecessarily executing commands for libraries/apps that are not affected by the changes.
Note this doesn’t only apply to e2e tests, but also to unit tests, linting and building.
More speed: never test the same code twice, with Nx Cloud
Nx’s affected commands already help a lot to reduce CI time. But still, based on your changes & workspace library structure, you might still end up affecting a lot of libraries and thus running lots of builds/tests.
However, you could even improve this further by never running the same command twice. How? With computation caching! Starting from v9.2 Nx has a built-in computation caching mechanism. Whenever your run a command, Nx analyzes the involved source files and configuration and caches the result. If you happen to run the same command again, without any changes to your src files, Nx simply outputs the previous result from the cache. You can read more about it here.
This greatly speeds up your local runs. But you can even make this cache distributed and remote by subscribing and configuring Nx Cloud. That way you can share your cache with co-workers or your CI server.Nx Cloud keeps track of all the executed commands, indexes the involved environment and library contents as well as the execution result. Whenever some of your work mates executed a particular set of Cypress tests and you happen to run them as well, instead of loosing precious time, waiting for the test run to finish, you’ll get the cached result from your co-worker.
This also works for CI! Here’s what it looks like when the build pipeline has already executed the tests and I re-run them locally again on my machine:
All this doesn’t need any particular configuration but can lead to significant time savings. Here’s a graph of running Cypress e2e tests on CI. On day 18 Nx Cloud has been activated, immediately leading to drastic time savings from around ~30 minutes down to ~15 minutes in a couple of days.
Curious? Get access to Nx Cloud on https://nx.app and make your Cypress tests blazingly fast!
Conclusion
In this article we learned about how we can leverage Nx together with Cypress to automate our testing setup. We’ve seen
- how to setup a new React based workspace with Cypress e2e tests for our apps
- how to generate Nx libraries with Storybook support
- how to share custom Cypress commands
- how to leverage Storybook to create Cypress based tests for our React components
- how TypeScript can help explore the Cypress API via code completion support
- how to speed up Cypress test runs with Nx’s affected commands
- how to never run Cypress tests twice with the support of Nx Cloud
You can check out the source code used in this article at https://github.com/juristr/nx-react-cypress-blogpost.
Update as of 5/15/20: nx v9.3 has been released, with further improvements and updates. See below for a video walkthrough of the topics discussed in this article: