Back to the homepage

Cypress – introduction

End-to-end testing is a methodology for testing an application from a user’s perspective. Its goal is to ensure that the application behaves according to expectations (frontend to backend). In this series of articles, we will show you how to write e2e tests for an Angular application using Cypress.

Firstly, I would like to thank the co-author Mateusz Stefanczyk who supported me while I was writing this article.

Also, thanks to Norbert Pioterczak for updating the article and describing the latest versions.

About the series 

Due to the complexity of the subject of testing, as well as Cypress itself, we decided to divide the article into several smaller parts to make it easier for you to understand this topic and separate certain elements.

Parts of the series 
  • Introduction
    a brief description of the framework, installation, configuration, Desktop GUI, simple interface tests, best and worst practices for UI testing
  • Integration tests
    fixtures, integration testing, best and worst integration testing practices
  • Extension of e2e tests
    custom commands, logging with GitHub as a social provider
  • Do we need another e2e framework?
    cypress vs selenium, replacing protractor

What is Cypress? 

Cypress is a framework (open source) for writing integration and e2e tests, which stands out due to its high comprehensiveness. It provides us with all the necessary tools – we can start writing tests right after installation. What’s more, no additional libraries are required to be installed. Here are the features that distinguish it from other available frameworks:

  • it doesn’t t rely on webdriver (unlike selenium, which uses webdriver, more on that in the next part of the article)
  • all-in-one – Cypress includes a test environment, assertion libraries, and tools for mocking and stubbing:
      • Mocha – a feature-rich, JavaScript framework for testing
      • Chai – a BDD/TDD assertion library
      • Sinon – a mocking library for JavaScript
  • automatically waits for (extension of this point later):
      • loading the DOM until the elements start to be visible
      • completion of animation
      • completion of XHR and AJAX calls.
  • time-traveling – Cypress takes snapshots during testing. This lets us track each step of our test. In case an error occurs, it allows us to very quickly identify the source, e.g. verifying if the right button was pressed, etc.
  • provides all commands under one global cy object. This makes writing tests intuitive and simple. 
  • allows us to write tests in JavaScript and TypeScript.
  • functional GUI, which enables the preview of the executed tests in real-time and debugging with DevTools.
  • automatically reloads tests, when changes are made in the test file.

Adding Cypress to your project

To add Cypress to an existing Angular project, use the command:

Files directory

  • Fixtures – are used as static data in our tests. We most often use them for mocking web requests (xhr/ajax), where we indicate which fixture to use as a data source for the server request response, e.g.  

The line above will cause all GET requests sent in the URL, containing /example/ in its path as a response, to return data from the example.json file located in the structure of the fixtures.

We can also download fixtures by using the cy.fixture() command. It is used to load data contained in the indicated file. Downloading data this way is helpful for making any assertions.

  • .js
  • .jsx
  • .coffe
  • .cjsx
  • Plugins– Cypress initially generates an index.js file in this directory that contains default plugins. The file is launched before each test. Plugins allow us to modify and extend Cypress’ internal behavior.

As of Cypress version 10, the functionality has been withdrawn, due to the introduction of support for .js and .ts configuration files, which are now required. Configuration in JSON format has also been phased out. It has been replaced by the ‘setupNodeEvents()’ and ‘devServer’ configuration options. SetupNodeEvents() is the equivalent of an exported plugins.js file. Through this function, such a pre-written file can also be imported, although this is not recommended due to the lack of typing – useful for testing Angular applications.

  • Support – this directory is the perfect place to put in reusable functions, behaviors like custom commands or global configurations for our t.j. test mocking server queries.

In addition to those directories mentioned above, some folders can be generated when the test environment is launched, and contain for example recordings of our tests, screenshots, etc. It’s worth considering adding such folders to the .gitignore file.

Version 11 doesn’t create a directory structure by default (after installation using the method indicated above). A directory with a sample configuration appears only after starting cypress with the ‘cypress open’ command and selecting the testing method (in this case, for simplicity, e2e). Only then we get information about what files and directories will be created. These are:

– cypress.config.ts – the main configuration file, in which we specify configuration options if we want to change the default behavior of cypress – the basic configuration allows you to run tests without any problems

– cypress/fixtures/example.json – sample fixture file

– cypress/support/commands.ts – sample file in which we will eventually add our own commands available within the ‘cy’ wrapper

– cypress/support/e2e.ts – a sample file that loads before the e2e tests.

After adding a sample test from the GUI level, we are additionally presented with an e2e directory, which will contain spec.ts files (the default format used with Angular) describing each test.

The directory structure indicated above can be modified and freely configured.

Desktop GUI

The desktop interface is an application written in Electron that makes creating and debugging tests much easier. We can launch it in a classic project via cypress open, or if we use nrwl/nx, the desktop GUI is launched via ng e2e app-e2e (it is useful to add the –watch flag during development and debugging).

After launching, we will be shown a window where we can choose the type of tests. Currently, the choices include the e2e option, known from previous versions, and a new feature from version 10, Component Testing. We will focus on the first one. After clicking on the E2E Testing window for the first time, information about the necessary files that will be added (including a configuration file) will pop up.

We see this window one time, the next time we will go straight to the browser selection window.

Here we have a choice of both popular browsers (Chrome, Firefox) and Electron. After selecting a browser, we can already go to the list of tests. In case we don’t have any test scenarios yet, Cypress will suggest us to create a new one, or use a package of example ones.

For the purpose of this article, we will use a ready-made package called ‘Scaffold example specs’. After clicking the appropriate button, it will show us a list of example test scenarios.

After selecting a test, it will launch exactly what all the fuss is about – the Cypress window, with the individual tests listed. We will be able to observe the progress as well as the results on an open subpage.

The view, that shows running tests, offers far more functionalities. We have access to full test logs along with details on each step. What’s more, after hovering over a particular step, the application preview will display the application state/snapshot as it was at the time of a given command – a great facilitation during debugging.

Also it is worth mentioning that the Selector Playground, with which we can very quickly generate selectors for items in the current view, will always follow Cypress best practices.

Interface testing

Firstly, let’s focus on testing the interface itself excluding the API (integration tests in the second article of the series). You can start writing tests in your application – if Cypress isn’t configured in your project, in case of lack of nrwl/nx you can use schematics available there. The examples presented in this article are from a mini-application written specifically to present the test ( and it is the one we recommend using when going through our tutorial 🙂

W repozytorium razem z przykładową aplikacją dostaniemy Cypressa w starszej wersji. Nic nie stoi na przeszkodzie, żeby spróbować po zaznajomieniu się z obsługą Cypressa dodać najnowszą wersję i zweryfikować swoją wiedzę 🙂 

Let’s assume that we want to test the following view from our application, a simple login page:

Let’s create  signing.spec.ts in file e2e/src/integration and write our first block: 

describe is a Cypress method that contains one or more related tests. Each time you start writing a new set of tests for a given functionality you do so inside the describe block.

The next puzzle piece we come across is the it method and target test case code:

it acts as a wrapper for specific and individual tests. cy refers directly to Cypress – only this way we can access the offered methods. 

cy.visit(…) is a method that performs the action of switching to a route that is considered as an argument (this is the same as typing the URL into the browser. This is also how you should imagine Cypress tests – like commands for a regularuser).

cy.get(…) is a method for selecting elements on the page. It takes a selector as an argument (such as the one we generate through Selector Playground) and tells Cypress to grab the element that matches the selector. We can perform many more methods on the object that get returns, for example, the adopting argument – should – defines the requirement we are checking (you can read more about the selectors you can use in cy.get(…) in the best & bad practices section)

A test written this way always ensures that if an email input is left without a value, an error with a specific content will be displayed. 

At this point, we should mention the following key rule: tests should run individually – independently of each other. In practice, this means that in our case we will want to use cy.visit(‘/’) in each test – to avoid repeating this code we can use beforeEach in the describe block.

Another thing worth checking is whether the login button is disabled when our form is invalid and whether the button is enabled when the form is correctly validated. To simplify our testing, we create a small Utils and this way we’ll shorten the writing of the Util. In the e2e/src/support folder, we create get-data-test.util.ts with the following contents:

After this action, our newly written test for the login button looks as follows:

Try to do a similar test yourself in our application.

The login interface should display lorem ipsum after checking the “Remember me” checkbox. Try to write a test for this part of the login yourself (it will be almost identical to the first error test when the email input is empty). 

After writing the test cases, we can check their result in the Desktop GUI.

We can see step by step how each of the consecutive requirements was met and how the tests were qualified as successful.

Examples of selectors

cy.get(‘input’) – find element with input tag

cy.get(‘.menu’) – find element with .menu class 

cy.get(‘#menu’) – find element with .menu id 

cy.get(‘a[href=”login]’) – find a element with href=”login”attribute 

cy.get(‘[data-test=”sidebar’) – find element with data-test=”sidebar”attribute

Automatic waiting

As mentioned in the previous paragraph – Cypress automatically waits until an element reaches the expected state. Now that you have learned the basics of writing interface tests, it will be easier for us to delve into this topic 🙂 

In case of assertions (e.g. .should(‘be.disabled’)) Cypress will automatically wait a certain amount of time until our item reaches the expected state, so we don’t have to worry about specifying precisely when the item should meet the set requirement. The waiting mentioned above involves constant checking on set expectations (more on that here). 

Many functions have built-in default requirements, for example .get() and .find() wait for the element to exist in the DOM, .type() waits for an element that enables writing and .click() waits for an element with which to interact. The requirements mentioned above are covered by an automatic expectation, for example, when using the .click() function, Cypress makes sure that the element can be interacted with. If not, it will wait until the element:

  • isn’t hidden,
  • isn’t covered,
  • isn’t disabled,
  • isn’t during animation.

This guarantees a good developer experience and reduces the number of things we have to consider when writing tests. You can read more about the points mentioned above in the official documentation.

Best & bad practices

Selecting HTML elements

correct: using data-* attribute to isolate selectors from CSS or JS changes

incorrect: using elements in the selectors, that can change 

Almost every test you will write will contain element selectors. Compliance with the rule of using data-* attributes will avoid many potential problems that could arise for example when changing the CSS class of a particular element.

 What’s the best way to avoid this problem?

  • don’t select elements based on their CSS atribbutes (id, class, tag),
  • don’t target elements by textContent,
  • use data-* attribute.

How does it work?

Let’s suppose we have this button we want to test:

Selector When to use
cy.get(‘button’) never – lack of context, heavily generic
cy.get(‘.call-button’) never – attached to the styles, high probability of change
cy.get(‘#call-button’) rarely – but still attached to styles
cy.get(‘[name=call-button]’) rarely – violates HTML semanticity 
cy.contains(‘Call me’) dependently – based on the value
cy.get(‘[data-cy=call-button’) always – isolates from all changes 


Text Content

After looking at the table above, you may feel slightly confused and ask yourself:

> Why is the cy.contains green, if we were previously given advice “not to target elements by textContent?

The answer is simple, but not obvious. Do you want the test to fail when the element value changes?

> If the test fails, use cy.contains()

>If the test passes, use data-* attribute


Playground Selector automatically generates/proposes selectors according to good practices.

Visiting external sites

correct: test only what you can control. Avoid using external servers; but if you have to, always use cy.request() instead. 

incorrect: Testing/opening sites or servers that we cannot control.

The first thing many people do is involve an external server in their tests. You might want this in the following cases, for example:

  • testing various auth providers with OAuth,
  • verifying whether changes appear on the external server,
  • checking if the email sent by “forgot password” has arrived.

Most likely you would try to use cy.visit() to navigate to other pages. However, you should never do this during testing because:

  • this is very time-consuming and slows down test writing
  • the external site can:
    • change its content,
    • have errors beyond your control,
    • detect that you are using a test script and block access (e.g., GitHub does this),
    • run A/B campaign,
    • prohibit such actions through its regulations. 

There are several strategies for dealing with such situations, which you can explore in our next article in the series on Cypress (available soon).


About the author

Adrian Zaorski

Philanthropist, virtuoso of Angular and welding with non-fusible argon shielded electrode. In his spare time he lies in the bathtub.

Do you want to write articles for our blog together with us?
Join us and create valuable content for Angular fans with!

Leave a Reply

Your email address will not be published. Required fields are marked *