What you can do with Cypress
- End-to-End testing in
BDD
(expect
/should
) andTDD
(assert
) assertion styles
it('adds todos', () => { cy.visit('https://todo.app.com') cy.get('[data-testid="new-todo"]') .type('write code{enter}') .type('write tests{enter}') // confirm the application is showing two items cy.get('[data-testid="todos"]').should('have.length', 2) })
- Component testing
import TodoList from './components/TodoList' it('contains the correct number of todos', () => { const todos = [ { text: 'Buy milk', id: 1 }, { text: 'Learn Component Testing', id: 2 }, ] cy.mount(<TodoList todos={todos} />) // the component starts running like a mini web app cy.get('[data-testid="todos"]').should('have.length', todos.length) })
- API testing
it('adds a todo', () => { cy.request({ url: '/todos', method: 'POST', body: { title: 'Write REST API', }, }) .its('body') .should('deep.contain', { title: 'Write REST API', completed: false, }) })
Cypress features
- See what happened at each step of testing (time travel)
- Debug directly using Developer Tools
- Automatic waiting without using of wait commands or manual async/await calls
- Verify and control the behavior of functions using Spies, Stubs, and Clocks
- Easily control, stub, and test edge cases without involving your server
- Consistent results, because it does not use Selenium or WebDriver
- View screenshots taken automatically on failure, or videos of your entire test suite
- Cross browser Testing
- Parallelize your test suite on Cypress cloud
- Discover and diagnose unreliable tests using Cypress Cloud’s Flaky test management.
Important Cypress concepts
- Cypress is built on top of Mocha, Sinon and Chai.
- Cypress Test Runner runs all your tests in a real browser installed on your local system
- Cypress commands don’t do anything at the moment they are invoked, but rather enqueue themselves to be run later in serial order
- Cypress commands are asynchronous but you should not/can’t use async/await or loops
Cypress vs. Selenium vs. WebDriver
Selenium and most other testing tools run outside of the browser and execute remote commands across the network. Cypress does the opposite as it is executed in the same run loop as the application. Behind Cypress is a Node server process which constantly communicates, synchronizes, and perform tasks in accordance with Cypress. This gives us the ability to respond to the application’s events in real time, while at the same time work outside of the browser for tasks that require a higher privilege. Cypress also operates at the network layer by reading and altering web traffic on the fly. This enables Cypress to not only modify everything coming in and out of the browser. Cypress ultimately controls the entire automation process from top to bottom, which puts it in the unique position of being able to understand everything happening in and outside of the browser. Because Cypress is installed locally on your machine, it can additionally tap into the operating system for automation tasks. This makes performing tasks such as taking screenshots, recording videos, general file system operations and network operations possible.
Your test code can access all the same objects that your application code can.
Installing Cypress
Change into your project root folder (package.json
must exist) and run
yarn add cypress -dev
Verify that it was correctly installed by running Cypress:
yarn run cypress open
or in case this makes trouble (it did for me, see below) run that instead:
node_modules\.bin\cypress open
For convenience you can add a script to package.json
:
{ "scripts": { "cy:open": "cypress open", "cy:run": "cypress run" } }
Folder structure
The first time you run cypress in your project it will create a default folder structure in your project root:
E2E: /cypress.config.ts /cypress/fixtures/example.json /cypress/support/commands.ts /cypress/support/e2e.ts Component: /cypress.config.ts /cypress/fixtures/example.json /cypress/support/commands.ts /cypress/support/component.ts /cypress/support/component-index.html
- Fixtures are external pieces of static data that can be used by your tests. You would typically use them with the
cy.fixture()
command and most often when you’re stubbing Network Requests. - Commands allow you to write common functions, e.g. to login()
- Support file runs before every single spec file. The names of the support files are fix:
component.ts
ande2e.ts
. This is a great place to put global configuration and behavior that modifies Cypress.
Asset folders
The following asset folders are created in certain situations:
/cypress - /downloads (when a test downloads a file) - /screenshots (via cy.screenshot() or if test fails) - /videos
You probably want to .gitignore
those folders.
Test isolation vs. caching in session
When running component tests, Cypress always resets the browser context, that is clearing cookies, localStorage and sessionStorage. Cypress supports enabling or disabling test isolation in end-to-end testing to describe if a suite of tests should run in a clean browser context or not. When enabled, Cypress resets the browser context before each test.
Cypress allows for browser context to be cached with cy.session()
. This means as a user, you only need to perform authentication once for the entirety of your test suite, and restore the saved session between each test. That means you do not have to visit a login page, type in a username and password and wait for the page to load and/or redirect for every test you run. You can accomplish this once with cy.session()
and if needed, cy.origin()
.
// Caching session when logging in via page visit cy.session(name, () => { cy.visit('/login') cy.get('[data-test=name]').type(name) cy.get('[data-test=password]').type('s3cr3t') cy.get('form').contains('Log In').click() cy.url().should('contain', '/login-successful') }) // Caching session when logging in via API cy.session(username, () => { cy.request({ method: 'POST', url: '/login', body: { username, password }, }).then(({ body }) => { window.localStorage.setItem('authToken', body.token) }) })
Writing E2E tests
- Tests can be written in .ts, .tsx,
.js
,.jsx
,.coffee
and.cjsx
- You can
import
orrequire
both npm packages and local relative modules.
Create a test file mytest.spec.js
in my-app/cypress/e2e
.
// mytest.spec.js describe('My test suite', () => { it('should test something', () => { }) })
context()
is identical to describe()
and specify()
is identical to it()
.
Test configuration
You can configure things such a baseUrl
, browser
, requestTimeout
, retries and many more.
describe(name, config, fn) context(name, config, fn) it(name, config, fn) specify(name, config, fn)
Querying elements by CSS selectors
cy.get('.my-selector'); cy.get('#elementId'); cy.get(':checkbox') // and all other possible CSS selectors
Adjusting async retry logic timeout
Cypress wraps all DOM queries with an asynchronous retry-and-timeout logic. That’s why the following will not work:
// This will not work! Cypress does not return the element synchronously. const $cyElement = cy.get('.element')
Default timeouts
- 4 seconds for selecting elements
- 6 seconds for cy.visit() and cy.exec()
- cy.wait() 5 seconds when waiting for routing alias plus 3 seconds for the server response
You can define your own timeout if an element needs more time to appear:
// Give this element 10 seconds to appear cy.get('.my-slow-selector', { timeout: 10000 })
Here is an example with two assertions:
cy.get('.mobile-nav').should('be.visible').and('contain', 'Home')
It is important to understand that the default timeout of 4 seconds applies to the command, not the assertions. In other words: Cypress gives a total of 4 seconds to assert that the element .mobile-nav
is visible
and contains the text Home
, and not a total of 4 + 4 + 4 = 12 seconds.
You can also set a global timeout.
Access a selected element
cy.get('#some-link') .then(($myElement) => { // grab its href property const href = $myElement.prop('href') // strip out the 'hash' character and everything after it return href.replace(/(#.*)/, '') }) .then((href) => { // href is now the new subject // which we can work with now })
Querying elements by text content
// Find an element in the document containing the text 'New Post' cy.contains('New Post') // Find an element within '.main' containing the text 'New Post' cy.get('.main').contains('New Post')
Instead of cy.get(selector).should('contain', text)
or cy.get(selector).contains(text)
chain, it is recommended using cy.contains(selector, text)
which is retried automatically as a single command.
You probably do not want to use this approach if your text changes frequently or you use translations.
Filter elements
cy.get('td').filter('.users')
Interacting with elements
Cypress will wait until a selected element becomes “actionable”, e.g. waits until is not hidden, not being covered, not being disabled, not being animated.
cy.get('textarea.post-body').type('This is an excellent post.') // others: .blur() .focus() .clear() .check() .uncheck() .select() .dblclick() .rightclick()
Example of pressing keys (e.g. Enter) into an element:
cy.get('.new-todo').type('todo A{enter}')
An events’ coordinates are fired at the center of the element, but most commands enable you to change the position it’s fired to.
cy.get('button').click({ position: 'topLeft' })
Debugging interactions
cy.get('button').debug().click()
Now open up the dev tools in the test runner and debug from there.
In the web dev console you have access to subject
variable which points to the selected element. You can invoke functions, e.g. subject.text()
.
Assertions
Cypress uses Chai to handle assertions. Here is the full assertion reference.
Cypress will automatically wait until these assertions pass. This prevents you from having to know or care about the precise moment your elements eventually do reach this state.
cy.get(':checkbox').should('be.disabled') // others: .should('have.class', 'form-horizontal') .should('not.have.value', 'US')
Assert existence of an element
.should('exist') .should('not.exist') .should('be.visible') .should('not.be.visible')
Example of asserting that elements disappear after an action:
// now Cypress will wait until this // <button> is not in the DOM after the click cy.get('button.close').click().should('not.exist') // and now make sure this #modal does not exist in the DOM // and automatically wait until it's gone! cy.get('#modal').should('not.exist')
Assert if CSS class or CSS property exists
.should('have.class', 'my-list-item') .should('have.css', 'background-color', 'blue')
Example of asserting that an object contains a specific path of properties:
cy.window() .its('app.model.todos') // retried .should('have.length', 2)
Assert text content
.invoke('text') .should('contain') .should('not.contain')
Default assertions
With Cypress, you don’t have to assert to have a useful test. This is because many commands have a built in Default Assertion which offer you a high level of guarantee. For example cy.visit(‘http://somesite.org’) expects the page to send text/html content with a 200 status, or that cy.request() expects that a server provides a response etc.
A full list of assertion can be found in this reference.
Chaining multiple assertions
cy.get('#header a') .should('have.class', 'active') .and('have.attr', 'href', '/users')
or use the long way – if you want to change the element in some way prior to making the assertion:
cy.get('tbody tr:first').should(($tr) => { expect($tr).to.have.class('active') expect($tr).to.have.attr('href', '/users') })
Here is another advanced example in which we assert that all p-elements have a specific text:
cy.get('p').should(($p) => { // massage our subject from a DOM element // into an array of texts from all of the p's let texts = $p.map((i, el) => { return Cypress.$(el).text() }) // jQuery map returns jQuery object // and .get() converts this to an array texts = texts.get() // array should have length of 3 expect(texts).to.have.length(3) // with this specific content expect(texts).to.deep.eq([ 'Some text from first p', 'More text from second p', 'And even more text from third p', ]) })
You have to make sure that the entire function can be executed multiple times without side effects.
Best practice: Alternate commands and assertions
it('adds two items', () => { cy.visit('/') cy.get('.new-todo').type('todo A{enter}') cy.get('.todo-list li') // command .should('have.length', 1) // assertion .find('label') // command .should('contain', 'todo A') // assertion cy.get('.new-todo').type('todo B{enter}') cy.get('.todo-list li') // command .should('have.length', 2) // assertion .find('label') // command .should('contain', 'todo B') // assertion })
Aliases
Instead of selecting an element by writing selectors such as '.my-selector > p.something'
many times you can instead define a short alias for convenience.
cy.get('.my-selector') .as('myElement') // sets the alias .click() /* many more actions */ cy.get('@myElement') // re-queries the DOM as before (only if necessary) .click()
It is recommended that you alias elements as soon as possible instead of further down a chain of commands.
Aliases when using Mocha hooks
beforeEach(() => { // alias the $btn.text() as 'text' cy.get('button').invoke('text').as('text') }) it('has access to text', function () { this.text // access is synchronously, but cannot use with arrow function cy.get('@text') // access is asynchronously and also we can use arrow functions })
Triggering events on selected elements / Dynamically generate tests
You use .trigger():
describe('if your app uses jQuery', () => { ;['mouseover', 'mouseout', 'mouseenter', 'mouseleave'].forEach((event) => { it('triggers event: ' + event, () => { // if your app uses jQuery, then we can trigger a jQuery // event that causes the event callback to fire cy.get('#with-jquery') .invoke('trigger', event) .get('#messages') .should('contain', 'the event ' + event + 'was fired') }) }) })
Fixtures
beforeEach(() => { // alias the users fixtures cy.fixture('users.json').as('users') }) it('utilize users in some way', function () { // use the special '@' syntax to access aliases // which avoids the use of 'this' cy.get('@users').then((users) => { // access the users argument const user = users[0] // make sure the header contains the first // user's name cy.get('header').should('contain', user.name) }) })
Running Tests
- Cypress suggests running test files individually by clicking on the spec filename to ensure the best performance.
- But Cypress also allows you to run all spec files together by clicking the “Run all specs” button. This mode is equivalent to concatenating all spec files together into a single piece of test code.
- You can also run a subset of all specs by entering a text search filter.
Watching for changes
When running in using cypress open, Cypress watches the filesystem for changes to your spec files. Soon after adding or updating a test Cypress will reload it and run all of the tests in that spec file.
Skipping tests
Can be used as placeholder, when you have not yet completely written the test. Those tests will have the state pending
.
describe('TodoMVC', () => { it('is not written yet') it.skip('adds 2 todos', function () { cy.visit('/') cy.get('.new-todo').type('learn testing{enter}').type('be cool{enter}') cy.get('.todo-list li').should('have.length', 100) }) xit('another test', () => { expect(false).to.true }) })
Run in debug mode
DEBUG=cypress:server:specs npx cypress open ## or DEBUG=cypress:server:specs npx cypress run
Conditional Testing
The only way to do conditional testing on the DOM is if you are 100% sure that the state has “settled” and there is no possible way for it to change.
In all other cases you can still achieve conditional testing without relying on the DOM. You have to anchor yourself to another piece of truth that is not mutable.
To write tests for A/B Testing you could program your server that always returns a fixed state (A or B) depending on a parameter that you send along:
cy.visit('https://app.com?campaign=A') cy.visit('https://app.com?campaign=B')
Alternatively, if your server saves the campaign with a session, you could ask your server to tell you which campaign you are on.
// this sends us the session cookies cy.visit('https://app.com') // assuming this sends us back // the campaign information cy.request('https://app.com/me') .its('body.campaign') .then((campaign) => { // runs different cypress test code // based on the type of campaign return campaigns.test(campaign) })
Another way to test this is if your server sent the campaign in a session cookie that you could read off.
cy.visit('https://app.com') cy.getCookie('campaign').then((campaign) => { return campaigns.test(campaign) })
Another valid strategy would be to embed data directly into the DOM – but do so in a way where this data is always present and query-able. It would have to be present 100% of the time, else this would not work.
cy.get('html') .should('have.attr', 'data-campaign') .then((campaign) => { return campaigns.test(campaign) })
The official doc has more strategies of conditional testing.
Environment variables
Option 1: Setting env var via command line
E.g. export CYPRESS_MY_VARIABLE="hello"
on Linux (must start with CYPRESS_
).
Option 2: Passing as argument
cypress open --env MY_VARIABLE="hello"
Option 3: Adding to Cypress.json
{ "env": { "MY_ENV_VARIABLE" : "hello" } }
Option 4: Create cypress.env.json
{ "MY_ENV_VARIABLE" : "hello" }
Accessing env var in tests
const my = Cypress.env('MY_VARIABLE')
Remember to not commit secret info to your repository.