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

Screenshots and Videos

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')
  })
})
                    
                

Example site: NgDoc.io

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

Questions?