Skip to content

How to Create Better Test Coverage Using Cypress 10

This article was written over 18 months ago and may contain information that is out of date. Some content may be relevant but please refer to the relevant official documentation or available resources for the latest information.

Testing is an integral part of software development, and it is important that all developers learn best testing practices. Jordan Powell, DX engineer at Cypress and Angular Google Developer Expert, has some tips to share with developers on how to write better tests using Cypress 10.

In this article, we will take a look at Powell’s tips for writing end-to-end tests, component tests, and advanced testing patterns in Cypress. If you want to learn more from Jordan Powell, please check out his Advanced Cypress JS Drop training.

Table of Contents

Why is Testing important?

Software testing identifies issues within the application, and helps ensure that only high quality products are shipped to the user.

Here are Jordan Powell’s reasons on why testing is important, and how it helps developers:

Documentation: Good testing coverage will result in developers creating stronger test plans and better QA testing for new releases.

Confidence: Writing good tests allows developers to build new features in confidence because the applications are working as intended.

Safe Refactoring: Good test coverage leads to less refactoring down the road, and provides developers more time to work on new features.

Improved UX: Good test coverage provides better UX (User Experience) for the end user because the application is working as intended.

Types of Testing

E2E, Integration, and Unit testing are three common methods for testing software applications.

Screenshot 2022-12-13 110451

Unit Test: Unit testing serves as the foundation of the test pyramid. If you're working in a functional language, a unit will most likely be a single function. Your unit tests will call a function with different parameters and ensure that it returns the expected values. In an object-oriented language, a unit can range from a single method to an entire class.

Integration test: Integration testing is where individual components of your application are tested as a group.

End-to-end testing: E2E Testing is a technique that tests your app from the web browser through to the back end of your application, as well as testing integrations with third-party APIs and services. These types of tests are great at making sure your entire app is functioning as a cohesive whole.

A new take on the testing pyramid

In Jordan Powell’s Advanced Cypress JS Drop training, he challenges developers to rethink the traditional testing pyramid, and believes that component testing should also be included.

Component Testing with Cypress 10 allows developers to test individual components quickly, regardless of its complexity. Component tests differ from end-to-end tests in that instead of visiting a URL to pull up an entire app, a component can be "mounted" and tested on its own.

Screenshot 2022-12-13 110700

Differences between end-to-end testing and component testing

Here are a few key differences between End-to-end and component testing:

End-to-end testingComponent testing
The entire application and all of its layers are testedOnly the independent components are tested
Testing can be done by developers and QA TeamsTesting is done by the developers
Often requires a complex setupNo extra configuration for CI(Continuous Integration) environments needed
Initialization command: cy.visit(url)Initialization command: cy.mount(<MyComponent>)

Jordan Powell’s best practices for E2E testing

Don’t use HTML Native selectors

It is not good practice to use element selectors, id attributes, or class attributes when writing End-to-end tests. Using HTML Native selectors can lead to team members refactoring tests later on, or changing attributes which could affect the tests.

Jordan Powell recommends using data attributes that can inform the team that this is a test attribute. When other team members need to change anything, they will be aware of the function of the attribute and what it may affect.

Screenshot 2022-12-21 122927

Use Closures

Jordan warns against assigned return values for Cypress assertions, and recommends using closures to access the Commands yield like this:

cy.get('button').then((btn) => {
    // store the button's
    const txt = btn.text()

    // submit a form
    cy.get('form').submit()

    // compare the two button's text
    // and make sure they are different

    cy.get('button').should((btn2) => {
        expect(btn2.text()).not.to.eq(txt)
    })
})

// these commands run after all of the
// other previous commands have finished
cy.get(...).find(...).should(...)

Independent Test

It is not good practice to combine related tests. Jordan Powell suggests that tests should be run independently from one another without sharing or relying on other test states. If test suites are related in any way, run the related properties or methods before each of the test runs, like in the code below:

describe('my form', () => {
    beforeEach(() => {
        cy.visit('/users/new')
        cy.get('#firstname').type('Hassan')
        cy.get('#lastname').type('Sani')
    })

    it('display form validation', () => {
        // clear out the first name
        cy.get('#firstname').clear()
        cy.get('form').submit()
        cy.get('#errors').should('contain', 'First name is required’)
    })

    it('can submit a valid form', () => {
        cy.get('form').submit()
    })
})

Use Route Aliases

Another recommended practice is to use route aliases to guard Cypress against proceeding until an explicit condition is met.

// route aliases
cy.intercept('**/api/users').as('usersRequest')
cy.wait('@usersRequest')

// assertions
cy.get('button').click()
cy.wait(4000) // <- this is unnecessary
cy.get('p').contains('hello world')

Setting a Global baseUrl

It is bad practice to use hardcoded URL strings for the baseUrl because it will eventually fail when the environment changes. Instead, Powell recommends using a config file that holds the environment baseUrl.

Jordan Powell’s Best Practices for Component Testing

All of the code examples in this next section will be for Angular, but Cypress component testing works for all major Frontend JavaScript frameworks.

Default Config Mount

Component testing takes two parameters: the component to mount, and the object for configuration.

A Custom Mount Config can boost flexibility. It ships a default mount config which you can use, but to manage multiple modules and import without adding too many boilerplate codes, it is recommended to have a custom mount config for specific instances.

import { HttpClientModule } from '@angular/common/http';
import { SharedModule } from 'libs/shared/shared';
import { mount } from 'cypress/angular';

type MountParams = Parameters<typeof mount>

Cypress.Commands.add('mount', (component: MountParams[0], config: MountParams[1] = {}) => {
  return mount(component, {
    ...config,
    imports: [
      ...config.imports,
      HttpClientModule,
      SharedModule
    ]
  })
})

Cypress Intercept

Component testing handles the individual components you are testing at a particular time, to access external API it is recommended to use cy.intercept. According to the Doc Cy.intercept can be used to passively listen for matching routes without manipulating the request or its response in any way.

it('should show bad login message when credentials are invalid', () => {
  cy.intercept('POST', '/auth', {
    statusCode: 401,
  });

  cy.mount('<app-login-form></app-login-form>');
  cy.get('button').contains('Sign in').click();

  cy.get('input[type=email]').type('bad@email.com');
  cy.get('input[type=password]').type('badpass');
  cy.get('button').contains('Sign in').click();

  cy.contains('Invalid username or password');
});

Use createOutputSpy

When working with eventEmitter for components that may contain forms or other elements with events, createOutputSpy will automatically create an EventEmitter and set up the spy on it's .emit() method.

it('should show bad login message when credentials are invalid', () => {
  cy.intercept('POST', '/auth', {
    statusCode: 401,
  });

  cy.mount('<app-login-form></app-login-form>',
     componentProperties: {
       onLogin: createOutputSpy('onLoginSpy'),
     }
   );
  cy.get('button').contains('Sign in').click();

  cy.get('input[type=email]').type('bad@email.com');
  cy.get('input[type=password]').type('badpass');
  cy.get('button').contains('Sign in').click();

  cy.contains('Invalid username or password');
});

Jordan Powell’s Advanced Cypress Patterns

Session

When testing for routes or components that require authentication Cypress provides cy.session which eliminates creating functions in your test to check for sessions on every call to the route or component. cy.session caches the sessions, and it will skip the login process for testing components that need the sessions on a second call.

Cypress.Command.add("login", (username, password) => {
    cy.session(
        [username, password],
        () => {
            cy.request({
                method: 'POST',
                url: '/login',
                body: {username, password},
            }).then(({body}) => {
                window.localStorage.setItem('authToken', body.token)
            })
        }
        {
            validate() {
                cy.request('/whoami').its('status').should('eq', 200)
            }
        }
    )
})

Origin

Running tests for projects that require a visit to multiple different domains could get CORS, or a limitation of visits, due to browser environment. Cypress provides Origin to help bypass some of the browser limitations.

const sentArgs = { username: 'username', password: 'password@254' }
cy.origin(
    'supersecuredsite.com',
    // Send the args here...
    { args: sentArgs },
    // ...and receive them at the other end here!
    ({username, password}) => {
        cy.visit('/login')
        cy.get('input#username').type(username)
        cy.get('input#password').type(password)
        cy.contains('button', 'Login').click()
    }
)

Conclusion

In this article, we looked at Jordan Powell’s tips for writing End-to-end tests, component tests, and advanced testing patterns in Cypress.

I recommend watching the full video for Jordan Powell’s Advanced Cypress JS Drop training.

Are you excited about the Cypress Component Testing? Let us know on Twitter.

This Dot is a consultancy dedicated to guiding companies through their modernization and digital transformation journeys. Specializing in replatforming, modernizing, and launching new initiatives, we stand out by taking true ownership of your engineering projects.

We love helping teams with projects that have missed their deadlines or helping keep your strategic digital initiatives on course. Check out our case studies and our clients that trust us with their engineering.

Let's innovate together!

We're ready to be your trusted technical partners in your digital innovation journey.

Whether it's modernization or custom software solutions, our team of experts can guide you through best practices and how to build scalable, performant software that lasts.

Prefer email? hi@thisdot.co