I'm so done with E2E testing... until now
(An introduction to cypress.io)
Jesse Sanders
@JesseS_BrieBug
jesse.sanders@briebug.com
Who am I?
Who is writing E2E Tests?
Test tools
- Selenium
- WebDriver
- Protractor
- Nighthwatch
- (Others)
E2E is Hard
- Take too long to run
- Fail intermittently/flakey
- Timing issues/hacks
- Developers give up
Web Applications in the old days
Web Applications have evolved
- Server Side Rendering
- Full Page Refresh
- JQuery
- Ajax
- Client Side MVCs
Challenges
- Snapshot at moment in time
- Magic/Seed data
-
Cryptic errors/stack traces
-
No dev tools during tests
E2E Testing Sucks
is so easy...
- Setup
- Authoring
- Running
- Debugging
Setup
npm install cypress --save-dev
// or use our schematic
ng add @briebug/cypress-schematic
npx cypress open // npm >v5.2
Built in Libraries
- Sinon
- jQuery
- Underscore
- Blob, minimatch, moment, Promise
Authoring
- Examples are added to every project
- https://example.cypress.io/
- Easy and logical
Visit -> Query -> Action -> Should
describe('My actions tests', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/actions')
})
it('.focus() - focus on a DOM element', () => {
// https://on.cypress.io/focus
cy.get('.action-focus')
.focus()
.should('have.class', 'focus')
.prev().should('have.attr', 'style', 'color: orange;')
})
});
Actions
describe('My actions tests', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/actions')
})
it('actions', () => {
cy.get('button').click().dblclick();
cy.get('input#keyword').type('redux{enter}').clear();
cy.get('input#firstName').focus().blur();
cy.get('#iAgree').check().uncheck();
})
});
Assertions
// implicit subjects
cy.get('#header a')
.should('have.class', 'active')
.and('have.attr', 'href', '/users')
// or explicit subjects
cy.get('tbody tr:first').should(($tr) => {
expect($tr).to.have.class('active')
expect($tr).to.have.attr('href', '/users')
})
Handling errors
- No more cyptic errors
- Easy to read output
- Hints about why
Our first test
// remove example tests
rm -fr cypress/integration/examples
// create a new file
touch cypress/integration/home-page-spec.js
// paste this into the new spec
describe('The Home Page', function() {
it('successfully loads', function() {
cy.visit('http://localhost:4200')
})
})
Start the server
ng serve
Let's have Cypress start the server!
- NO!
- Antipattern
-
`cy.exec` can only run tasks that eventually exit
- Port conflicts
Best Practice Tip
- cypress.json - set baseurl
- warns when server not running
- can be overriden using --config
- Eliminates hardcoded urls
{
"baseUrl": "http://localhost:4200"
}
npx cypress run --config baseUrl=http://localhost:4300
Is that it?
Where E2E fails
- So slow - can be hours
- Seed/Magic data
- Tough to test edge cases
- Fragile tests
Introducing Stubbed Responses
Stubbing Responses
- Recommended for most tests
- Have one true E2E, stub the rest
- Tests are way faster
- No magic data seed/setup
Stubbing is Easy
cy.server(); // start the server
cy.route({
method: 'GET', // Route all GET requests
url: '/users/*', // that have a URL that matches '/users/*'
response: [] // and force the response to be: []
})
Stubbing solves a lot of problems
- Fast Tests
- Consistent results
- True E2E can be smoke tests
But there's more!
Fixtures
cy.server()
// we set the response to be the activites.json fixture
cy.route('GET', 'users/*', 'fixture:users.json');
//or
cy.fixture('users.json').as('usersJSON');
cy.route('GET', 'users/*', '@usersJSON');
Waiting for calls to return
cy.server();
// create aliases
cy.route('activities/*', 'fixture:activities').as('getActivities');
cy.route('messages/*', 'fixture:messages').as('getMessages');
// visit the dashboard, which should make requests that match
// the two routes above
cy.visit('http://localhost:8888/dashboard');
// pass an array of Route Aliases that forces Cypress to wait
// until it sees a response for each request that matches
// each of these aliases
cy.wait(['@getActivities', '@getMessages']);
// these commands will not run until the wait command resolves above
cy.get('h1').should('contain', 'Dashboard');
Autocomplete - wait for call
cy.server()
cy.route('search/*', [{item: 'Book 1'}, {item: 'Book 2'}]).as('getSearch')
cy.get('#autocomplete').type('Book')
// this yields us the XHR object which includes
// fields for request, response, url, method, etc
cy.wait('@getSearch')
.its('url').should('include', '/search?query=Book')
cy.get('#results')
.should('contain', 'Book 1')
.and('contain', 'Book 2')
Summary
- UI Tests can be dependable
- E2E in small amounts OK
- Stubbing is the way to go!
- Complex interactions manageable
BrieBug - We're here to help
- Angular Experts
- Trusted Guidance
- Strategy and Implementation
- Proven Client Experience
Jesse Sanders
@JesseS_BrieBug
jesse.sanders@briebug.com