Marmicode
Blog Post
Younes Jaaidi

Testing Angular Components Using Cypress

by Younes Jaaidi • 
Feb 18, 2021 • 6 minutes
Testing Angular Components Using Cypress

Whatever framework you are using, and even without frameworks, component testing is a challenging topic as there is no one-size-fits-all approach.

One of the first challenges is picking a testing framework. In this blog post, we will focus on Jest, and Cypress which are gaining popularity in both JavaScript and Angular communities.

Jest Limitations

While staying simple and easy to set up compared to other tools, Jest provides a nice panel of features like its feature-rich API, the interactive watch mode, human-readable reports, native parallelization, and its flexible mocking system. These Developer Experience enhancements encourage us to write tests and can act as efficiency-boosters.

All of these features will generally make Jest a nice fit for testing behavior, business logic, and interactions with other components and services, but when it comes to DOM testing, a new challenge arises.

Like any other unit under test, a component, or a block (a logical group made of a component and some or all of its children tree) has inputs, outputs, and interactions with other units. However, one of the main specificities of component testing is the interaction with the DOM. This particular interaction will often have the following properties:

  • it can be hard to predict before implementing the component,
  • it can be complex or have an important surface,
  • different interactions can produce the same result. This is why this interaction (i.e. DOM interaction) can be considered as an implementation detail that should not spill out in the tests.

A good example of this is a component that hides some content depending on a condition. As you probably imagine, this could be implemented in lots of different ways:

<div>
  <h1 *ngIf="condition">Marmicode</h1>
</div>

or

<div *ngIf="condition">
  <h1>Marmicode</h1>
</div>

or

<div>
  <h1 [class.hidden]="!condition">Marmicode</h1>
</div>

or

<div [class.hidden]="!condition">
  <h1>Marmicode</h1>
</div>

Testing the existence of the h1 or the presence of the hidden class on the div or the h1 would mean that we are testing an implementation detail. Are we even sure that the hidden class really exists?

What we really want to test is the visibility of the h1. (By the way, the test should not even have to know that it's an h1 tag, but this is another topic. Let's keep this for another post.)

Luckily, there is a library extending Jest with DOM matchers that help to solve this issue: @testing-library/jest-dom

Still, even though DOM testing with Jest is possible, the Developer eXperience is not at its best. Indeed, Jest is not running in a browser so we can't produce a visual output (without some complex hacks like outputting the HTML in a file and opening a browser or starting a web server and taking screenshots with puppeteer etc...).

  • no visual debugging: the most common way of debugging DOM tests in Jest is to dump and analyze HTML snapshots.
  • no time travel: as there is no native visual debugging, there is no easy way to visualize how every action affects the tested component or block.
  • no visual regression testing: without visual rendering, and real browsers, we can't detect visual regressions (e.g. CSS issues).

Cypress

Cypress initially focuses on Functional, End-to-End, Smoke tests, etc... I just call them wide tests. My definition of a wide test is any test that matches one of the following conditions:

  • the test execution time takes more than 100ms,
  • when the test fails, it takes more than 1 minute to locate the file or function containing the root cause of the issue,
  • parallelization needs some effort (e.g. creating distinct accounts / instances / workspaces),
  • some outputs are hard to predict (e.g. number of items that will be displayed).

In opposition to the Jest limitations presented before, Cypress offers the following features and advantages:

  • visual debugging: Cypress runs on some real browsers (Firefox, Chromium-based: Chrome, Edge, Electron) so you can easily debug the DOM.
  • time travel: Cypress saves DOM snapshots after every command so you can easily inspect the DOM before and after every action.
  • visual regression testing: You can easily extend your tests with visual regression tests. Some tools like Percy will not only compare screenshots, but they will render the DOM snapshot on different browsers and viewport dimensions then compare them to the previous builds.

Yet, as mentioned before, Cypress focuses on wide tests. It is designed to visit a web application at a given URL using cy.visit() then interact with it at your will. This is when we start wondering how we could test a single component or block.

Cypress + Storybook Combination Limitations

One of the most common approaches is combining Cypress with UI component explorers like Storybook. Interestingly, every Storybook story (i.e. a component's scenario) is loaded in a preview iframe, meaning that we can visit the preview iframe of any story directly using Cypress.

cy.visit('/iframe.html?id=blogpost--default'); cy.get('h1').contains('Testing Angular Components Using Cypress')

We finally managed to isolate components and blocks in Cypress but this approach also has its limitations:

  • less control: we have no way to control inputs, providers, child components, etc...
  • it needs exposing all tested components in Storybook: even though most components we will want to test using Cypress are presentational components that deserve to be exposed in Storybook, we might want to test some container components, blocks, or specific components that shouldn't be exposed in Storybook,
  • we might not need Storybook: if we don't already need Storybook, it might feel cumbersome to add it as a non-negligible layer in the path between Cypress and the tested components,
  • no watch mode: changing the code of a tested component or block will reload the Storybook preview but will not restart the test in Cypress automatically.

Cypress Component Testing

As you might have already guessed, there is a better way. Cypress provides a new feature called Cypress Component Testing allowing us, using some framework-specific libraries, to mount components directly in the tests without having to visit any page.

@jscutlery/cypress-mount

Imagine if we could write our tests in Cypress somehow like this?

import { TitleComponent } from '.../src/title.component';

it('should display default title', () => {
  mount(TitleComponent);
  cy.get('h1').contains('👋 Welcome');
});

Guess what! Now, you can, thanks to our new @jscutlery/cypress-mount library. @jscutlery/cypress-mount has an opinionated approach aiming to reduce the learning curve and help making trustworthy and maintainable tests.

Once set up, not only you can mount and test any Angular component but @jscutlery/cypress-mount comes with some useful features. Here are some of them:

Control Inputs

We can of course control the component inputs.

mount(TitleComponent, { inputs: {appName: 'Marmicode'} }); cy.get('h1').contains('👋 Welcome to Marmicode');

Override Providers

We can replace services with test doubles.

mount(TitleComponent, { providers: [ { provide: Settings, useValue: {greetings: '🇫🇷 Bienvenue'} } ] }); cy.get('h1').contains('🇫🇷 Bienvenue');

Mount Template

We can dynamically mount any custom template.

mount(`<mc-title></mc-title>`, { imports: [TitleModule], })

Storybook & Component Story Format support

@jscutlery/cypress-mount also supports Storybook stories and CSF, meaning that we can reuse and mount Storybook stories in Cypress. Ain't that hip!?

import { Love } from '.../love.stories.ts'; describe('Love', () => { it('should show some love', () => { mountStory(Love); cy.get('h1').contains('❤️'); }); });

Try it yourself

If you want to give it a quick try, you can retrieve the jscutlery/test-utils monorepo and run some demo tests locally.

git clone https://github.com/jscutlery/test-utils
npm install
npx nx e2e cypress-mount-integration --watch

You will then find some examples here packages/cypress-mount-integration/src/components/demo.spec.ts

Where to go from here?

As mentioned at the beginning of this blog post, there is no one-size-fits-all approach. Hence, don't run and migrate all your component tests to Cypress Component Testing 😉. Also, it is not unusual to write tests for one component with both Jest and Cypress Component Testing as they cover different parts.

While Jest and its tremendous ecosystem is still often a good choice, you should consider Cypress Component Testing when:

  • testing a presentational component or block,
  • testing visual regressions,
  • testing specific browser inconsistencies (e.g. CSS hacks we love),
  • components have stories in Storybook,
  • current tests are asserting HTML snapshots (in this case, the tests are coupled to implementation details).