Testing with Cypress
and Jest

Introduction

Types of Tests + Frameworks

  • Unit Tests
  • API Tests
    • Test Web APIs
    • Framework/tool examples: Postman πŸ”—
  • Load Tests
    • Test scalability and performance under load
    • Framework/tool examples: JMeter πŸ”—, Azure Load Testing πŸ”—
  • End-to-end testing
    • Simulate end user interaction by remote-controlling browser
    • Framework/tool examples: Webdriver πŸ”—, Cypress πŸ”—
  • See also State of JS πŸ”—

Webdriver vs. Cypress

  • Webdriver
    • Remote-control browser
    • Works over network
    • W3c recommendation
    • Works with all major browsers
    • Widely used (incl. large component vendors πŸ”—)
  • Cypress
    • E2e testing web applications while they are under development πŸ”—
    • Browser plugin, controlled by Node process πŸ”—
    • Locally installed, full control over host system
    • Open-source with liable-to-cost extras

Cypress Limitations πŸ”—

  • Limited browser support (Firefox, Chrome-family) πŸ”—
  • Cypress test run in the browser
    • Cannot directly import server-side libraries in test code
    • Options: Write plugins πŸ”—, use Node tasks πŸ”—
  • No/limited multi-tab and multi-browser support

What's unique about Cypress?

  • Fine control over browser because Cypress runs insideΒ the browser
  • Snapshots after each command
    • Relatively easy to diagnose problems
  • No need for writing async code manually
    • Cypress specs build test commands
    • Cypress commands are queued and
      asynchronously executed at a later time πŸ”—
    • Async behavior (e.g. waiting) is built-in

Get Started!

Install Cypress πŸ”—,
optionally install recommended VSCode extensions πŸ”—,
add Cypress to Angular πŸ”—

Cypress CLI πŸ”—

Command Description
cypress open Opens the Cypress app
cypress run Runs Cypress tests (headlessly)
cypress info Prints information about Cypress environment
cypress verify Checks Cypress installation
cypress version Prints version info

Demo
Time!

Exercise: First E2E Test

# Create new folder, navigate to it
npm init
npm install -D cypress
# Add npm script for `cypress open` and `cypress run`

# Start cypress
# Select E2e -> configures your cy environment

# Generate new, empty spec for Chrome
describe('My First Test', () => {
  it('Does not do much!', () => {
    expect(true).to.equal(true)
  })
})
# Run test
# Change test so that it fails
# Rerun test

Exercise: First E2E Test

describe('My First Test', () => {
  it('Gets, types and asserts', () => {
    cy.visit('https://example.cypress.io')

    cy.contains('type').click()

    // Should be on a new URL which
    // includes '/commands/actions'
    cy.url().should('include', '/commands/actions')

    // Get an input, type into it and verify
    // that the value has been updated
    cy.get('#email1')
      .type('fake@email.com')
      .should('have.value', 'fake@email.com')
  })
})
# Run test
# Change test so that it fails
# Rerun test

Exercise: First E2E Test

# Install typescript
npm install -D typescript

# Add tsconfig.json in `cypress` folder 
{
  "compilerOptions": {
    "target": "es5",
    "lib": ["es5", "dom"],
    "types": ["cypress", "node"]
  },
  "include": ["**/*.ts"]
}
# Rename spec.cy.js to .ts

# Reopen cypress to see TypeScript test running

Selection, Naviation

$

Hello jQuery

  • Cypress bundles jQuery πŸ”—
  • cy.get() is like $('...')
    • Returns Chainable<T>
    • Many methods of ChainableΒ behave exactly like jQuery counterparts
    • E.g. cy's findΒ πŸ”— and Β jQuery's find πŸ”—
    • Learn more by learning jQuery's tree traversal functions πŸ”—
  • Cypress selectors are like jQuery's selectors πŸ”—
    • Best practices for Cypress selectors πŸ”—
    • TL;DR: Use data-*Β attributes wherever possible
  • Access jQuery directly using Cypress.$ πŸ”—
<!-- HTML -->
<button id="main" class="btn btn-large" name="submission"
        role="button" data-testid="submit">
  Submit
</button>
// Cypress
cy.get('[data-testid="submit"]').click();

Asynchronous Operations

  • Cypress wraps all DOM queries with retry-and-timeout logic
    • ⚠️ Make sure your testing code is idempotent
  • No error if e.g. ...
    • ...DOM not yet loaded
    • ...XHR not finished
    • ...framework bootstrapping in progress
  • Customize timeouts if necessary πŸ”—
    • Option: cy.get('...', { timeout: 10000 })
  • Get DOM element once it becomes available: .thenΒ πŸ”—
    • Note Cypress.$ has to be executed in .then πŸ”—

Demo
Time!

Exercise: Querying

<!-- app.component.html -->

<hello name="{{ name }}"></hello>
<p>Start editing to see some magic happen :)</p>

<button data-test-cy="main-button">Do something</button>
it('Querying', () => {
  cy.visit('https://angular-ivy-saxmjf.stackblitz.io')

  cy.get('#promptToRun', { timeout: 600000 }).find('button').click()

  cy.get('[data-test-cy="main-button"]')
    .should('have.text', 'Do something')
})

Interactions

Interacting with elements

  • Clicking, typing πŸ”—
  • State of element is checked before interaction πŸ”—
  • Complex drag-and-drop interactions with .trigger πŸ”—
  • Tip: Use .asΒ to create an alias for later access to subject πŸ”—

Assertations

  • Assertations comes from bundled libraries like Chai
  • Default and explicit assertations
    • Default assertations πŸ”—
    • Use .should('not.exist')Β to reverse assertations
  • Full list of assertations πŸ”—
  • Assertations in command chain
    • .should()
    • .and()
  • Assertations with explicit subjects πŸ”—
    • expect()

Demo
Time!

Exercise: TS, Cmds, Interactions

  • Delete/remove support/commands.js
  • Create folder support/commands
  • Create index.d.ts in support folder
export {};

declare global {
  namespace Cypress {
    interface Chainable {
      skipStackblitzProtection(): Chainable<Element>;
    }
  }
}

Exercise: TS, Cmds, Interactions

  • Create skipStackblitzProtection.tsΒ in support/commands
Cypress.Commands.add('skipStackblitzProtection', () => { 
    cy.get('#promptToRun', { timeout: 600000 }).find('button').click();
});
  • Change support/e2e.js
import "./commands/skipStackblitzProtection";

Exercise: TS, Cmds, Interactions

it('Querying', () => {
  cy.visit('https://angular-ivy-saxmjf.stackblitz.io')

  cy.skipStackblitzProtection()

  cy.get('[data-test-cy="main-button"]')
    .should('have.text', 'Do something')
})

it('Calculates correctly', () => {
  cy.visit('https://angular-basic-calculator.stackblitz.io/')

  cy.get('.button-1').as('1').click()
  cy.get('.button-0').click()
  
  cy.get('.button-a').click()
  cy.get('@1').click()
  
  cy.get('.equals').click()
  
  cy.get('.display').should('have.text', 10 + 1)
})

Test Organization

Folders πŸ”—

Folder Description
e2e Test files
fixtures Static data used by tests
support Support files running before each spec

Organize Tests

  • Folder for each page
    • E.g. orders, settings, account
  • Inside, spec for each test use case
    • E.g. account/register_spec.js
    • Test structure πŸ”—
  • `shared` folder for shared component tests
    • E.g. header bar

Troubleshooting Tests

Troubleshooting Tests πŸ”—

  • Use the debugger, it just works
  • Check the browser's console window
    • Also when clicking on commands like .type()
  • Use .debug()Β to trigger breakpoint and get detailed information about a yielded object
  • Β 

Server

Dealing with Server

  • Dealing with servers is tricky
    • E.g. logging in, persistence layer
  • Strategy: Stub all server requests πŸ”—
    • No longer e2e, a lot of work
    • Often pays off
  • Strategy: Seeded data on the server πŸ”—
    • E.g. pre-created user, well-defined test data πŸ”—
    • Cypress has certain auth server built-in πŸ”—
    • Real e2e
    • ⚠️ Tests might influence each other
  • Strategy: Setup unique data before each test
    • A lot of work, not simple
  • Mix strategies as you like/need

Stubbing Requests

  • Intercepting requests with .intercept() πŸ”—
  • Use fixtures for pre-recorded responses πŸ”—
  • Wait for expected requests πŸ”—

Jest

Jest

  • Modern, fast, widely used testing framework for JS/TS
  • Use jest-preset-angular for Jest + AngularπŸ”—
  • Simple Jasmine tests work without any changes
    • itΒ is alias for Jest's testΒ πŸ”—
  • Pros πŸ‘
    • Fine testing framework with great API
    • No need for Karma
    • Well documented
    • Widely used
  • Cons πŸ‘Ž
    • Doesn't come out of the box in Angular
    • Requires extra dependencies

Demo
Time!

Jest in Angular

  • Remove Jasmine and Karma from package.json
  • Remove karma.conf.js and src/test.ts
  • Remove testΒ area from angular.json
  • Install Jest πŸ”—
    • npm install --save-dev jest jest-preset-angular @types/jest
  • Configure jest-preset-angular as shown in πŸ”—
  • Set "esModuleInterop": trueΒ in tsconfig.json
  • Replace ng testΒ with jestΒ in package.json script
  • Good summary of whole process in πŸ”—

Expects and Matchers πŸ”—

  • Expect return expectation object πŸ”—
  • Call matchers on expectation object
  • Async code πŸ”—
    • Call expect/matchers in .then()
    • Use async/await

Setup and Teardown πŸ”—

  • Use before/afterEachΒ for setup and teardown per test
  • Use before/afterAllΒ for one-time setup and teardown per file
  • Group tests using describe
    • ​Setup/teardown methods are scoped in test group
  • Use beforeEachΒ to configure TestBed in Angular πŸ”—

Mocking πŸ”—

  • Jest can mock functions and modules
  • Can partly be used to replace Angular's mocking features
  • Consider ng-mocks πŸ”—
  • Reference:

Further topics

  • Snapshot testing πŸ”—
    • Renders HTML or UI component
    • Compares against baseline
    • Problem: Changes in rendered HTML (particularly 3rd party controls)
    • Alternative: Angular DOM testing, e2e testing
  • Timer mocks πŸ”—
    • Replace JS's timeout/interval functions
    • Control timers programmatically to speed up long-running tests
    • Reference: Fake Timers πŸ”—