The Fuzzy Line Between End-to-End And Component Tests

Gleb Bahmutov

Sr Director of Engineering

QA Global Summit 22.2

AGENDA

  • End-to-End Sudoku test
  • Why E2E is hard
  • Unit to Component frontend test
  • Component tests for framework X
  • Component test to E2E test
  • The End

Speaker: Gleb Bahmutov PhD

C / C++ / C# / Java / CoffeeScript / JavaScript / Node / Angular / Vue / Cycle.js / functional programming / testing

Learn how to test the web applications

using my courses at

https://cypress.tips/courses

Join others to fight the climate crisis

Gleb Bahmutov

Sr Director of Engineering

Let's test

the game

modes

npm start

npx cypress open

npm i -D cypress

const { defineConfig } = require('cypress')

module.exports = defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3000',
  },

  component: {
    ...
  },
})

cypress.config.js

it('changes the number of filled cells', () => {
  cy.visit('/')
  cy.get('select[name=status__difficulty-select]').should(
    'have.value',
    'Easy',
  )
  cy.get('.game__cell--filled').should('have.length', 45)

  cy.get('select[name=status__difficulty-select]').select(
    'Medium',
  )
  cy.get('.game__cell--filled').should('have.length', 40)

  cy.get('select[name=status__difficulty-select]').select(
    'Hard',
  )
  cy.get('.game__cell--filled').should('have.length', 30)
})

cypress/e2e/modes.cy.js

The game modes test

See

the game

like a player user

Can we

test

the game

play?

It is hard

to control

the random

game board

😢

We can

use the

Hint

button

it('fills each empty cell using Hint', () => {
  cy.visit('/')
  cy.get('.game__cell.game__cell--filled').should(
    'have.length',
    45,
  )
  cy.get('.game__cell')
    .not('.game__cell--filled')
    .each(($cell) => {
      cy.wrap($cell, { log: false }).click()
      cy.get('.status__action-hint').click()
    })

  cy.contains('.overlay__text', 'You solved it').should(
    'be.visible',
  )
})

cypress/e2e/hint.cy.js

it('fills each empty cell using Hint', () => {
  cy.visit('/')
  cy.get('.game__cell.game__cell--filled').should(
    'have.length',
    45,
  )
  cy.get('.game__cell')
    .not('.game__cell--filled')
    .each(($cell) => {
      cy.wrap($cell, { log: false }).click()
      cy.get('.status__action-hint').click()
    })

  cy.contains('.overlay__text', 'You solved it').should(
    'be.visible',
  )
})

cypress/e2e/hint.cy.js

Click on the cell

it('fills each empty cell using Hint', () => {
  cy.visit('/')
  cy.get('.game__cell.game__cell--filled').should(
    'have.length',
    45,
  )
  cy.get('.game__cell')
    .not('.game__cell--filled')
    .each(($cell) => {
      cy.wrap($cell, { log: false }).click()
      cy.get('.status__action-hint').click()
    })

  cy.contains('.overlay__text', 'You solved it').should(
    'be.visible',
  )
})

cypress/e2e/hint.cy.js

Click on the hint

it('fills each empty cell using Hint', () => {
  cy.visit('/')
  cy.get('.game__cell.game__cell--filled').should(
    'have.length',
    45,
  )
  cy.get('.game__cell')
    .not('.game__cell--filled')
    .each(($cell) => {
      cy.wrap($cell, { log: false }).click()
      cy.get('.status__action-hint').click()
    })

  cy.contains('.overlay__text', 'You solved it').should(
    'be.visible',
  )
})

cypress/e2e/hint.cy.js

The hint test

  • Random data

  • Server data

  • Timers and clocks

  • Conditional testing

  • Unclear test requirements

Making E2E hard

Let's

test the

Timer

it('Timer shows 10 seconds', () => {
  cy.visit('/')
  for (let k = 0; k < 10; k++) {
    cy.contains('.status__time', `00:0${k}`)
  }
})

cypress/e2e/timer-clock.cy.js

How does the Timer component show 15 minutes?

it('Timer shows 15 minutes', () => {
  cy.visit('/')
  cy.contains('.status__time', '15:00', {
    timeout: 900_000,
  })
})

Wait 15 minutes...

There Must Be A Better Way

How do we test smaller pieces of code?

import React, { useState, useEffect } from 'react'
import { useSudokuContext } from '../context/SudokuContext'
import moment from 'moment'

export const formatTime = ({ hours, minutes, seconds }) => {
  if (typeof seconds === 'undefined') {
    return '00:00'
  }
  let stringTimer = ''

  stringTimer += hours ? '' + hours + ':' : ''
  stringTimer += minutes ? (minutes < 10 ? '0' : '') + minutes + ':' : '00:'
  stringTimer += seconds < 10 ? '0' + seconds : seconds

  return stringTimer
}

export const Timer = (props) => { ... }

src/components/Timer.js

import { formatTime } from '../../src/components/Timer'

it('formats the time', () => {
  expect(formatTime({})).to.equal('00:00')
  expect(formatTime({ seconds: 3 })).to.equal('00:03')
  expect(formatTime({ minutes: 55, seconds: 3 })).to.equal(
    '55:03',
  )
  expect(
    formatTime({ hours: 110, minutes: 55, seconds: 3 }),
  ).to.equal('110:55:03')
})

cypress/e2e/format-time.cy.js

import { Timer } from '../../src/components/Timer'

it('shows the time', () => {
  <Timer minutes={15} />
  cy.contains('.status__time', '15:00')
})

I want this!

// imports framework-specific component
// needs to bundle all its dependencies
// needs to "start" the framework
import { Timer } from '../../src/components/Timer'

We are not in cy.visit('Kansas') anymore

npm start

npx cypress open

Configure Cypress component testing

Supported frameworks

const { defineConfig } = require('cypress')

module.exports = defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3000',
  },
  component: {
    devServer: {
      framework: 'create-react-app',
      bundler: 'webpack',
    },
  },
})

cypress.config.js

import React from 'react'
import { Timer } from './Timer'
import '../App.css'
import { SudokuContext } from '../context/SudokuContext'
import moment from 'moment'

describe('Timer', () => {
  it('sets the clock to the given value', () => {
    const timeGameStarted = moment().subtract(
      900,
      'seconds',
    )

    cy.mount(
      <SudokuContext.Provider value={{ timeGameStarted }}>
        <section className="status">
          <Timer />
        </section>
      </SudokuContext.Provider>,
    )
    cy.contains('15:00')
  })
})

src/components/Timer.cy.js

  • 58ms test
  • live app
  • DOM snapshots

See the component running

import React from 'react'
import { Timer } from './Timer'
import '../App.css'
import { SudokuContext } from '../context/SudokuContext'
import moment from 'moment'

describe('Timer', () => {
  it('sets the clock to the given value', () => {
    const timeGameStarted = moment().subtract(
      900,
      'seconds',
    )

    cy.mount(
      <SudokuContext.Provider value={{ timeGameStarted }}>
        <section className="status">
          <Timer />
        </section>
      </SudokuContext.Provider>,
    )
    cy.contains('15:00')
  })
})

src/components/Timer.cy.js

Cypress "understands"  how to bundle and mount your framework component

import React from 'react'
import { Timer } from './Timer'
import '../App.css'
import { SudokuContext } from '../context/SudokuContext'
import moment from 'moment'

describe('Timer', () => {
  it('sets the clock to the given value', () => {
    const timeGameStarted = moment().subtract(
      900,
      'seconds',
    )

    cy.mount(
      <SudokuContext.Provider value={{ timeGameStarted }}>
        <section className="status">
          <Timer />
        </section>
      </SudokuContext.Provider>,
    )
    cy.contains('15:00')
  })
})

src/components/Timer.cy.js

Import app or component CSS and use the markup close to the app

without App.css or markup

import React from 'react'
import { Timer } from './Timer'
import '../App.css'
import { SudokuContext } from '../context/SudokuContext'
import moment from 'moment'

describe('Timer', () => {
  it('sets the clock to the given value', () => {
    const timeGameStarted = moment().subtract(
      900,
      'seconds',
    )

    cy.mount(
      <SudokuContext.Provider value={{ timeGameStarted }}>
        <section className="status">
          <Timer />
        </section>
      </SudokuContext.Provider>,
    )
    cy.contains('15:00')
  })
})

src/components/Timer.cy.js

Import app or component CSS and use the markup close to the app

with App.css and markup

import React from 'react'
import { Timer } from './Timer'
import '../App.css'
import { SudokuContext } from '../context/SudokuContext'
import moment from 'moment'

describe('Timer', () => {
  it('sets the clock to the given value', () => {
    const timeGameStarted = moment().subtract(
      900,
      'seconds',
    )

    cy.mount(
      <SudokuContext.Provider value={{ timeGameStarted }}>
        <section className="status">
          <Timer />
        </section>
      </SudokuContext.Provider>,
    )
    cy.contains('15:00')
  })
})

src/components/Timer.cy.js

Pass the component data using props or other mechanisms

Timer

Numbers

Difficulty

StatusSection

GameSection

Game

App

import React from 'react'
import { Numbers } from './Numbers'
import '../App.css'
import { SudokuContext } from '../context/SudokuContext'

it('shows the selected number', () => {
  cy.mount(
    <SudokuContext.Provider
      value={{ numberSelected: '8' }}
    >
      <div className="innercontainer">
        <section className="status">
          <Numbers
            onClickNumber={cy.stub().as('click')}
          />
        </section>
      </div>
    </SudokuContext.Provider>,
  )

  cy.contains('.status__number', '8').should(
    'have.class',
    'status__number--selected',
  )
  cy.contains('.status__number', '9').click()
  cy.get('@click').should(
    'have.been.calledOnceWithExactly',
    '9',
  )
})

Props + Provider are framework specific

src/components/Numbers.cy.js

import React from 'react'
import { Numbers } from './Numbers'
import '../App.css'
import { SudokuContext } from '../context/SudokuContext'

it('shows the selected number', () => {
  cy.mount(
    <SudokuContext.Provider
      value={{ numberSelected: '8' }}
    >
      <div className="innercontainer">
        <section className="status">
          <Numbers
            onClickNumber={cy.stub().as('click')}
          />
        </section>
      </div>
    </SudokuContext.Provider>,
  )

  cy.contains('.status__number', '8').should(
    'have.class',
    'status__number--selected',
  )
  cy.contains('.status__number', '9').click()
  cy.get('@click').should(
    'have.been.calledOnceWithExactly',
    '9',
  )
})

Props + Provider are framework specific

The rest of the test is Cypress API only

src/components/Numbers.cy.js

import React from 'react'
import { Numbers } from './Numbers'
import '../App.css'
import { SudokuContext } from '../context/SudokuContext'

it('shows the selected number', () => {
  cy.mount(
    <SudokuContext.Provider
      value={{ numberSelected: '8' }}
    >
      <div className="innercontainer">
        <section className="status">
          <Numbers
            onClickNumber={cy.stub().as('click')}
          />
        </section>
      </div>
    </SudokuContext.Provider>,
  )

  cy.contains('.status__number', '8').should(
    'have.class',
    'status__number--selected',
  )
  cy.contains('.status__number', '9').click()
  cy.get('@click').should(
    'have.been.calledOnceWithExactly',
    '9',
  )
})

Props + Provider are framework specific

The rest of the test is Cypress API only

src/components/Numbers.cy.js

What About Framework / Test Library X?

Follow: Murat Ozcan

Each React component has

  • React Testing Library spec
  • Cypress component spec

A huge collection of various React testing examples with matching Cypress component specs

No more framework-specific syntax

import React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { act } from "react-dom/test-utils";

import Toggle from "./toggle";

let container = null;
beforeEach(() => {
  // setup a DOM element as a render target
  container = document.createElement("div");
  document.body.appendChild(container);
});

afterEach(() => {
  // cleanup on exiting
  unmountComponentAtNode(container);
  container.remove();
  container = null;
});

it("changes value when clicked", () => {
  const onChange = jest.fn();
  act(() => {
    render(<Toggle onChange={onChange} />, container);
  });

  // get a hold of the button element, and trigger some clicks on it
  const button = document.querySelector("[data-testid=toggle]");
  expect(button.innerHTML).toBe("Turn on");

  act(() => {
    button.dispatchEvent(new MouseEvent("click", { bubbles: true }));
  });

  expect(onChange).toHaveBeenCalledTimes(1);
  expect(button.innerHTML).toBe("Turn off");

  act(() => {
    for (let i = 0; i < 5; i++) {
      button.dispatchEvent(new MouseEvent("click", { bubbles: true }));
    }
  });

  expect(onChange).toHaveBeenCalledTimes(6);
  expect(button.innerHTML).toBe("Turn on");
});
import React from "react";
import Toggle from "./toggle";

it("changes value when clicked", () => {
  cy.mount(<Toggle onChange={cy.stub().as('change')} />);

  // get a hold of the button element, and trigger some clicks on it
  cy.contains("[data-testid=toggle]", "Turn on").click()
  cy.get('@change').should('have.been.calledOnce')
  cy.contains("[data-testid=toggle]", "Turn off")
    .click()
    .click()
    .click()
    .click()
    .click()

  cy.get('@change').its('callCount').should('eq', 6)
  cy.contains("[data-testid=toggle]", "Turn on")
});

equivalent Cypress component test

https://on.cypress.io/api

No more framework-specific syntax

import React from "react";
import Toggle from "./toggle";

it("changes value when clicked", () => {
  cy.mount(<Toggle onChange={cy.stub().as('change')} />);

  // get a hold of the button element, and trigger some clicks on it
  cy.contains("[data-testid=toggle]", "Turn on").click()
  cy.get('@change').should('have.been.calledOnce')
  cy.contains("[data-testid=toggle]", "Turn off")
    .click()
    .click()
    .click()
    .click()
    .click()

  cy.get('@change').its('callCount').should('eq', 6)
  cy.contains("[data-testid=toggle]", "Turn on")
});

equivalent Cypress component test https://on.cypress.io/api

import { NavComponent } from './nav.component'
import { SigninService } from '../signin.service'

describe('NavComponent', () => {
  it('should create and show the links', () => {
    const signinService = new SigninService()
    cy.spy(signinService, 'login').as('login')
    cy.spy(signinService, 'logout').as('logout')
    cy.mount(NavComponent, {
      componentProperties: {
        signinService,
      },
    })

    cy.contains('a', 'HOME')
    cy.contains('button', 'LOG IN').should('be.visible').wait(1000).click()
    cy.get('@login').should('have.been.called')

    cy.contains('a', 'HOME')
    cy.contains('a', 'PROFILE').should('be.visible')
    cy.contains('button', 'LOG OUT').should('be.visible').wait(1000).click()
    cy.get('@logout').should('have.been.called')

    cy.contains('a', 'HOME')
    cy.contains('button', 'LOG IN').should('be.visible')
  })
})

src/app/nav/nav.component.cy.ts

Mount is framework specific

Cypress API

NavComponent

import { NavComponent } from './nav.component'
import { SigninService } from '../signin.service'

describe('NavComponent', () => {
  it('should create and show the links', () => {
    const signinService = new SigninService()
    cy.spy(signinService, 'login').as('login')
    cy.spy(signinService, 'logout').as('logout')
    cy.mount(NavComponent, {
      componentProperties: {
        signinService,
      },
    })

    cy.contains('a', 'HOME')
    cy.contains('button', 'LOG IN').should('be.visible').wait(1000).click()
    cy.get('@login').should('have.been.called')

    cy.contains('a', 'HOME')
    cy.contains('a', 'PROFILE').should('be.visible')
    cy.contains('button', 'LOG OUT').should('be.visible').wait(1000).click()
    cy.get('@logout').should('have.been.called')

    cy.contains('a', 'HOME')
    cy.contains('button', 'LOG IN').should('be.visible')
  })
})

src/app/nav/nav.component.cy.ts

🎁

import { NavComponent } from './nav.component'
import { SigninService } from '../signin.service'

describe('NavComponent', () => {
  it('should create and show the links', () => {
    const signinService = new SigninService()
    cy.spy(signinService, 'login').as('login')
    cy.spy(signinService, 'logout').as('logout')
    cy.mount(NavComponent, {
      componentProperties: {
        signinService,
      },
    })

    cy.contains('a', 'HOME')
    cy.contains('button', 'LOG IN').should('be.visible').wait(1000).click()
    cy.get('@login').should('have.been.called')

    cy.contains('a', 'HOME')
    cy.contains('a', 'PROFILE').should('be.visible')
    cy.contains('button', 'LOG OUT').should('be.visible').wait(1000).click()
    cy.get('@logout').should('have.been.called')

    cy.contains('a', 'HOME')
    cy.contains('button', 'LOG IN').should('be.visible')
  })
})

src/app/nav/nav.component.cy.ts

Angular

Examples for several frameworks

A Testing

Pyramid of Component Tests

Timer

Numbers

Difficulty

StatusSection

GameSection

Game

App

Overlay

import { Game } from './Game'
import { SudokuProvider } from './context/SudokuContext'
import { WinProvider } from './context/WinContext'
import { starting, solved } from '../cypress/fixtures/sudoku.json'

it('plays the game', () => {
  cy.mount(
    <SudokuProvider>
      <WinProvider>
        <Game initArray={starting} solvedArray={solved} />
      </WinProvider>
    </SudokuProvider>,
  )
  cy.get('.game__cell:not(.game__cell--filled)').should(
    'have.length',
    3,
  )
  starting.forEach((cell, index) => {
    if (cell === '0') {
      cy.get('.game__cell').eq(index).click()
      cy.contains('.status__number', solved[index])
        .click()
        .wait(500, { log: false })
    }
  })

  cy.contains('.overlay__text', 'You solved it').should('be.visible')
})

src/Game.cy.js

import { Game } from './Game'
import { SudokuProvider } from './context/SudokuContext'
import { WinProvider } from './context/WinContext'
import { starting, solved } from '../cypress/fixtures/sudoku.json'

it('plays the game', () => {
  cy.mount(
    <SudokuProvider>
      <WinProvider>
        <Game initArray={starting} solvedArray={solved} />
      </WinProvider>
    </SudokuProvider>,
  )
  cy.get('.game__cell:not(.game__cell--filled)').should(
    'have.length',
    3,
  )
  starting.forEach((cell, index) => {
    if (cell === '0') {
      cy.get('.game__cell').eq(index).click()
      cy.contains('.status__number', solved[index])
        .click()
        .wait(500, { log: false })
    }
  })

  cy.contains('.overlay__text', 'You solved it').should('be.visible')
})

src/Game.cy.js

import { Game } from './Game'
import { SudokuProvider } from './context/SudokuContext'
import { WinProvider } from './context/WinContext'
import { starting, solved } from '../cypress/fixtures/sudoku.json'

it('plays the game', () => {
  cy.mount(
    <SudokuProvider>
      <WinProvider>
        <Game initArray={starting} solvedArray={solved} />
      </WinProvider>
    </SudokuProvider>,
  )
  cy.get('.game__cell:not(.game__cell--filled)').should(
    'have.length',
    3,
  )
  starting.forEach((cell, index) => {
    if (cell === '0') {
      cy.get('.game__cell').eq(index).click()
      cy.contains('.status__number', solved[index])
        .click()
        .wait(500, { log: false })
    }
  })

  cy.contains('.overlay__text', 'You solved it').should('be.visible')
})

src/Game.cy.js

src/Game.cy.js

import React, { useState, useEffect } from 'react'
import { formatTime } from './Timer'

const useFetch = (url) => {
  const [data, setData] = useState([])
  const [loading, setLoading] = useState(url ? true : false)

  async function fetchData() {
    if (url) {
      const response = await fetch(url)
      const json = await response.json()
      setData(json)
      setLoading(false)
    }
  }

  useEffect(() => {
    if (!url) {
      return
    }
    fetchData()
  }, [url])

  return { loading, data }
}

export const Overlay = (props) => {
  const { loading, data } = useFetch(
    props.overlay && props.time ? '/times/' + props.time : null,
  )
  const className = props.overlay
    ? 'overlay overlay--visible'
    : 'overlay'
  return (
    <div className={className} onClick={props.onClickOverlay}>
      <h2 className="overlay__text">
        <div className="overlay__greeting">
          You <span className="overlay__textspan1">solved</span>{' '}
          <span className="overlay__textspan2">it!</span>
        </div>
        {loading && (
          <div className="overlay__loading">Loading...</div>
        )}

        {data.length > 0 && (
          <ul className="overlay__times">
            {data.map((item, index) => {
              return (
                <li
                  key={index}
                  className={item.current ? 'overlay__current' : ''}
                >
                  {formatTime(item)}
                </li>
              )
            })}
          </ul>
        )}
      </h2>
    </div>
  )
}

src/components/Overlay.js

import React, { useState, useEffect } from 'react'
import { formatTime } from './Timer'

const useFetch = (url) => {
  const [data, setData] = useState([])
  const [loading, setLoading] = useState(url ? true : false)

  async function fetchData() {
    if (url) {
      const response = await fetch(url)
      const json = await response.json()
      setData(json)
      setLoading(false)
    }
  }

  useEffect(() => {
    if (!url) {
      return
    }
    fetchData()
  }, [url])

  return { loading, data }
}

export const Overlay = (props) => {
  const { loading, data } = useFetch(
    props.overlay && props.time ? '/times/' + props.time : null,
  )
  const className = props.overlay
    ? 'overlay overlay--visible'
    : 'overlay'
  return (
    <div className={className} onClick={props.onClickOverlay}>
      <h2 className="overlay__text">
        <div className="overlay__greeting">
          You <span className="overlay__textspan1">solved</span>{' '}
          <span className="overlay__textspan2">it!</span>
        </div>
        {loading && (
          <div className="overlay__loading">Loading...</div>
        )}

        {data.length > 0 && (
          <ul className="overlay__times">
            {data.map((item, index) => {
              return (
                <li
                  key={index}
                  className={item.current ? 'overlay__current' : ''}
                >
                  {formatTime(item)}
                </li>
              )
            })}
          </ul>
        )}
      </h2>
    </div>
  )
}

src/components/Overlay.js

it('shows the loading element', () => {
  cy.intercept('GET', '/times/90', {
    delay: 1000,
    statusCode: 404,
    body: [],
  }).as('times')
  cy.mount(<Overlay overlay={true} time={90} />)
  cy.contains('.overlay__loading', 'Loading').should('be.visible')
  cy.wait('@times')
  cy.get('.overlay__loading').should('not.exist')
})
it('shows the loading element', () => {
  cy.intercept('GET', '/times/90', {
    delay: 1000,
    statusCode: 404,
    body: [],
  }).as('times')
  cy.mount(<Overlay overlay={true} time={90} />)
  cy.contains('.overlay__loading', 'Loading').should('be.visible')
  cy.wait('@times')
  cy.get('.overlay__loading').should('not.exist')
})
it('shows the top times', () => {
  cy.intercept('GET', '/times/90', {
    fixture: 'times.json',
  }).as('scores')
  cy.mount(<Overlay overlay={true} time={90} />)
  cy.wait('@scores')
  cy.get('.overlay__times li').should('have.length', 4)
  cy.contains('.overlay__times li', '01:30').should(
    'have.class',
    'overlay__current',
  )
})

My advice: Mock / spy on the public APIs (network, storage, cookies, DOM) from your component tests rather than the inner code.

 

It is ok to pass spies and stubs as props.

import { starting, solved } from '../fixtures/sudoku.json'

describe('Sudoku', () => {
  it('plays the same game', () => {
    // to play the same game, we will pass
    // the starting and the solved arrays
    // to the application via the "window" object
    cy.visit('/', {
      onBeforeLoad(window) {
        window.starting = starting
        window.solved = solved
      },
    })

    cy.intercept('GET', '/times/*', {
      fixture: 'times.json',
    }).as('scores')

    // our initial array only has 3 cells to fill
    cy.get('.game__cell:contains(0)').should('have.length', 3)
    starting.forEach((cell, index) => {
      if (cell === '0') {
        cy.get('.game__cell').eq(index).click()
        cy.contains('.status__number', solved[index])
          .click()
      }
    })

    cy.contains('.overlay__text', 'You solved it').should('be.visible')
    cy.wait('@scores')
    cy.fixture('times.json')
      .its('length')
      .then((n) => {
        cy.get('.overlay__times li').should('have.length', n)
      })

    cy.get('.overlay__times li.overlay__current').should('have.length', 1)
  })
})

Back to the Future End-to-End Tests

import { starting, solved } from '../fixtures/sudoku.json'

describe('Sudoku', () => {
  it('plays the same game', () => {
    // to play the same game, we will pass
    // the starting and the solved arrays
    // to the application via the "window" object
    cy.visit('/', {
      onBeforeLoad(window) {
        window.starting = starting
        window.solved = solved
      },
    })

    cy.intercept('GET', '/times/*', {
      fixture: 'times.json',
    }).as('scores')

    // our initial array only has 3 cells to fill
    cy.get('.game__cell:contains(0)').should('have.length', 3)
    starting.forEach((cell, index) => {
      if (cell === '0') {
        cy.get('.game__cell').eq(index).click()
        cy.contains('.status__number', solved[index])
          .click()
      }
    })

    cy.contains('.overlay__text', 'You solved it').should('be.visible')
    cy.wait('@scores')
    cy.fixture('times.json')
      .its('length')
      .then((n) => {
        cy.get('.overlay__times li').should('have.length', n)
      })

    cy.get('.overlay__times li.overlay__current').should('have.length', 1)
  })
})

Back to the Future End-to-End Tests

import { starting, solved } from '../fixtures/sudoku.json'

describe('Sudoku', () => {
  it('plays the same game', () => {
    // to play the same game, we will pass
    // the starting and the solved arrays
    // to the application via the "window" object
    cy.visit('/', {
      onBeforeLoad(window) {
        window.starting = starting
        window.solved = solved
      },
    })

    cy.intercept('GET', '/times/*', {
      fixture: 'times.json',
    }).as('scores')

    // our initial array only has 3 cells to fill
    cy.get('.game__cell:contains(0)').should('have.length', 3)
    starting.forEach((cell, index) => {
      if (cell === '0') {
        cy.get('.game__cell').eq(index).click()
        cy.contains('.status__number', solved[index])
          .click()
      }
    })

    cy.contains('.overlay__text', 'You solved it').should('be.visible')
    cy.wait('@scores')
    cy.fixture('times.json')
      .its('length')
      .then((n) => {
        cy.get('.overlay__times li').should('have.length', n)
      })

    cy.get('.overlay__times li.overlay__current').should('have.length', 1)
  })
})
import { Game } from './Game'
import { SudokuProvider } from './context/SudokuContext'
import { WinProvider } from './context/WinContext'
import { starting, solved } from '../cypress/fixtures/sudoku.json'

it('plays the game', () => {
  cy.mount(
    <SudokuProvider>
      <WinProvider>
        <Game initArray={starting} solvedArray={solved} />
      </WinProvider>
    </SudokuProvider>,
  )
  
  cy.intercept('GET', '/times/*', {
      fixture: 'times.json',
    }).as('scores')

  cy.get('.game__cell:not(.game__cell--filled)').should(
    'have.length',
    3,
  )
  starting.forEach((cell, index) => {
    if (cell === '0') {
      cy.get('.game__cell').eq(index).click()
      cy.contains('.status__number', solved[index])
        .click()
        .wait(500, { log: false })
    }
  })

  cy.contains('.overlay__text', 'You solved it').should('be.visible')
  cy.wait('@scores')
    cy.fixture('times.json')
      .its('length')
      .then((n) => {
        cy.get('.overlay__times li').should('have.length', n)
      })

    cy.get('.overlay__times li.overlay__current').should('have.length', 1)
})

End-to-End test

Component test

expect(formatTime({ seconds: 3 }))
  .to.equal('00:03')

Unit test

import { Component } from './Component'
cy.mount(<Component props=... />)
cy.get(...).click()

Component test

cy.visit('/')
cy.get(...).click()

End-to-End test

  • Small chunks of code like functions and classes
  • Front-end React / Angular / Vue / X components
  • Easy to test edge conditions
  • Web application
  • Easy to test the entire user flow

Benefits of Cypress Component Testing

Web Application Testability

Benefits of Cypress Component Testing

Use The Same Cy API for all tests

Benefits of Cypress Component Testing

Runs In The Browser

The Fuzzy Line Between End-to-End And Component Tests

Gleb Bahmutov

Sr Director of Engineering

@bahmutov

QA Global Summit 22.2

👏 Thank you 👏

The Fuzzy Line Between End-to-End And Component Tests

By Gleb Bahmutov

The Fuzzy Line Between End-to-End And Component Tests

Recently Cypress Test Runner has released v10 which adds full component testing support. Now you might have a question: when should I write an end-to-end test and when should I use a component test? If a component test runs as a "mini" web application, is it really a limited E2E test? In this talk, I will give my take on drawing the line between the types of tests to write to most effectively confirm the code is doing what it is supposed to do. Presented at QA Global Summit 2022, video at https://youtu.be/jJCDSSVyA9g

  • 1,655