Cypress is a great E2E testing tool. Here are a few great reasons to consider it:
Isolated installation possible.
Ships with TypeScript support out of the box.
Provides a nice interactive google chrome debug experience. This is very similar to how UI devs mostly work manually.
Has command - execution separation which allows for more powerful).
The steps provided in this installation process will give you a nice
e2e folder that you can copy/paste or as boiler plate for your organization.
Same steps presented in a video format over at my youtube channel.
Create an e2e directory, install cypress, TypeScript and setup the typescript and cypress config files:
mkdir e2ecd e2enpm init -ynpm install cypress typescriptnpx tsc --init --types cypress --lib dom,es6echo {} > cypress.json.
Add a few scripts to the
e2e/package.json file:
"scripts": {"cypress:open": "cypress open","cypress:run": "cypress run"},
Write your first test under
cypress/integration/basic.ts:
it('should perform basic google search', () => {cy.visit('');cy.get('[name="q"]').type('subscribe').type('{enter}');});
Now run
npm run cypress:open during development and
npm run cypress:run on your build server 🌹
Under the
e2e folder you now have these files:
/cypress.json: Configure cypress. The default is empty and that is all you need.
/cypress Subfolders:
/integration: All your tests.
Feel free to create tests under subfolders for better organization e.g.
/someFeatureFolder/something.spec.ts.
create a file
/cypress/integration/first.ts with the following contents:
describe('google search', () => {it('should work', () => {cy.visit('');cy.get('#lst-ib').type('Hello world{enter}')});});
Open the cypress IDE using the following command.
npm run cypress:open
And select a test to run.
You can run cypress tests in ci mode using the following command.
npm run cypress:runcy.get(`#${Ids.username}`).type('john')page.visit();page.username.type('john');
Cypress ships with (built in) chai and chai-query assertion libraries to help testing webpages. You use them with
.should command passing in the chainer as a string, replacing
.to.foo with
should('foo') e.g. with chai-jquery you would
expect($(#foo)).to.have.text('something'), with cypress you would
cy.get('#foo').should('have.text', 'something'):
cy.get('#foo').should('have.text', 'something')
You get intellisense for
shouldchainers as cypress ships with correct TypeScript definitions 👍🏻
The complete list of chainers is available here :
If you want something complex you can even use
should(callback) and e.g.
cy.get('div').should(($div) => {expect($div).to.have.length(1);expect($div[0].className).to.contain('heading');})// This is just an example. Normally you would `.should('have.class', 'heading')
TIP: cypress with do automatic retries on the callback as well, so they are just as flake free as standard string chainers.
Every function call in a cypress chain is a
command. The
should command is an assertion. It is conventional to start distinct category of chains and actions separately e.g.
// Don't do thiscy.get(/**something*/).should(/**something*/).click().should(/**something*/).get(/**something else*/).should(/**something*/)// Prefer separating the two getscy.get(/**something*/).should(/**something*/).click().should(/**something*/)cy.get(/**something else*/).should(/**something*/)
Some other libraries evaluate and run the code at the same time. Those libraries force you to have a single chain which can be nightmare to debug with selectors and assertions mingled in.
Cypress commands are essentially declarations to the cypress runtime to execute the commands later. Simple words: Cypress makes it easier.
The following shows an example:
cy.get('#foo')// Once #foo is found the following:.contains('Submit').click()// ^ will continue to search for something that has text `Submit` and fail if it times out.// ^ After it is found trigger a click on the HTML Node that contained the text `Submit`.
Cypress will automatically wait (and retry) for many async things e.g.
// If there is no request against the `foo` alias cypress will wait for 4 seconds automaticallycy.wait('@foo')// If there is no element with id #foo cypress will wait for 4 seconds automatically and keep retryingcy.get('#foo')
This keeps you from having to constantly add arbitrary timeout (and retry) logic in your test code flow.
Cypress has a concept of implicit assertion. These kick in if a future command is erroring because of a previous command. E.g. the following will error at
contains (after automatic retries of course) as nothing found can get
clicked:
cy.get('#foo')// Once #foo is found the following:.contains('Submit').click()// ^ Error: #foo does not have anything that `contains` `'Submit'`
In traditional frameworks you would get a horrible error like
click doesn't exist on
null. In Cypress you get a nice error
#foo does not contain
Submit. This error is a form of an implicit assertion.cy.visit('/')// wait for the callcy.wait('@load')// Now the data is loaded
You can also easily mock out a request response using
route:
cy.server().route('POST', '', /* Example payload response */{success:true});
You can assert requests without mocking using
route
onRequest /
onResponse e.g.
cy.route({method: 'POST',url: '',onRequest: (xhr) => {// Example assertionexpect(xhr.request.body.data).to.deep.equal({success:true});}})');
You can also use cypress to unit test your application code in isolation e.g.
import { once } from '../../../src/app/utils';// Laterit('should only call function once', () => {let called = 0;const callMe = once(()=>called++);callMe();callMe();expect(called).to.equal(1);});
If you are unit testing modules in your application you can provide mocks using
cy.stub e.g. if you want to ensure that
navigate is called in a function
foo:
foo.ts
import { navigate } from 'takeme';export function foo() {navigate('/foo');}
You can do this as in
some.spec.ts:
/// <reference types="cypress"/>import { foo } from '../../../src/app/foo';import * as takeme from 'takeme';describe('should work', () => {it('should stub it', () => {cy.stub(takeme, 'navigate');foo();expect(takeme.navigate).to.have.been.calledWith('/foo');});});
When you invoke a cypress command (or assertion) e.g.
cy.get('#something'), the function immediately returns without actually carrying out the action. What it does do, is informs the cypress test runner that you will need to carry out (execute) an action (in this case a
get) at some point.
You are basically building a command list that the runner will then go ahead and execute. You can verify this command - execution separation with a simple test, observe that you will see the
start / between / end
console.log statements execute immediately before the runner starts executing the commands:
/// <reference types="cypress"/>describe('Hello world', () => {it('demonstrate command - execution separation', () => {console.log('start');cy.visit('');console.log('between');cy.get('.gLFyf').type('Hello world');console.log('end');});});
This command execution separation has two big benefits:
The runner can execute the commands in a flake resistant manner with automatic retries and implicit assertions.
Allows you to write asynchronous code in a synchronous fashion without having to do a constant chaining which results in difficult to maintain code. statement in your application code and the test runner will stop on that just like standard web development.
Test code breakpoints: You can use the
.debug() command and cypress test execution will stop at it. Alternatively you can use a
debugger statement in a
.then command you can click on the
get in the command log to automatically
console.log any information you might need about the
.get('#foo') command (and similarly for any other commands you want to debug).):
Visual Testing :
Optionally set a
baseUrl in cypress.json to prevent an initial reload that happens after first
visit.