Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Speaker Deck
PRO
Sign in
Sign up for free
Connect.Tech 2020: Advanced Cypress Testing
Jeremy Fairbank
September 28, 2020
Programming
1
120
Connect.Tech 2020: Advanced Cypress Testing
Jeremy Fairbank
September 28, 2020
Tweet
Share
More Decks by Jeremy Fairbank
See All by Jeremy Fairbank
CodeMash 2020: Solving the Boolean Identity Crisis
jfairbank
1
110
CodeMash 2020: Practical Functional Programming
jfairbank
1
280
Connect.Tech 2019: Practical Functional Programming
jfairbank
0
220
Connect.Tech 2019: Solving the Boolean Identity Crisis
jfairbank
0
140
Lambda Squared 2019: Solving the Boolean Identity Crisis
jfairbank
0
42
All Things Open 2018: Practical Functional Programming
jfairbank
2
230
Connect.Tech 2018: Effective React Testing
jfairbank
1
110
Fluent Conf 2018: Building web apps with Elm Tutorial
jfairbank
2
710
Codestock 2018: Building a Resilient, API-driven Front-End with Elm
jfairbank
0
230
Other Decks in Programming
See All in Programming
Circuit⚡
monaapk
0
200
Showkase、Paparazziを用いたビジュアルリグレッションテストの導入にチャレンジした話 / MoT TechTalk #15
mot_techtalk
0
120
[2023년 1월 세미나] 데이터 분석가 되면 어떤 일을 하나요?
datarian
0
600
新卒でサービス立ち上げから Hasuraを使って3年経った振り返り
yutorin
0
230
Cloudflare WorkersでGoを動かすライブラリを作っている話
syumai
1
320
OSSから学んだPR Descriptionの書き方
fugakkbn
4
130
PHPアプリケーションにおけるアーキテクチャメトリクスについて / Architecture Metrics in PHP Applications
isanasan
1
260
Cloudflare Workersと状態管理
chimame
3
490
Micro Frontends with Module Federation @MicroFrontend Summit 2023
manfredsteyer
PRO
0
580
Use KMM to call the API of the National Tax Agency
akkeylab
0
300
OIDC仕様に準拠した Makuake ID連携基盤構築の裏側
ymtdzzz
0
570
Zynq MP SoC で楽しむエッジコンピューティング ~RTLプログラミングのススメ~
ryuz88
0
370
Featured
See All Featured
Helping Users Find Their Own Way: Creating Modern Search Experiences
danielanewman
10
1.3k
Responsive Adventures: Dirty Tricks From The Dark Corners of Front-End
smashingmag
239
19k
Faster Mobile Websites
deanohume
295
29k
Fontdeck: Realign not Redesign
paulrobertlloyd
74
4.3k
Distributed Sagas: A Protocol for Coordinating Microservices
caitiem20
318
19k
Put a Button on it: Removing Barriers to Going Fast.
kastner
56
2.5k
Producing Creativity
orderedlist
PRO
335
38k
Stop Working from a Prison Cell
hatefulcrawdad
263
18k
Six Lessons from altMBA
skipperchong
15
2.3k
Become a Pro
speakerdeck
PRO
6
3.2k
個人開発の失敗を避けるイケてる考え方 / tips for indie hackers
panda_program
29
7.9k
Writing Fast Ruby
sferik
613
58k
Transcript
Testing Advanced Cypress Testing Jeremy Fairbank @elpapapollo
@testdouble helps improves how the world build software. testdouble.com
Available in print or e-book programming-elm.com
Testing anti-patterns
Assumptions
Assumptions
Assumptions End-to-End
Assumptions End-to-End Real API
Assumptions End-to-End Real API Test Data
The App bit.ly/act-repo
Selector Syndrome
import albums from '../../../server/jazz-albums-test-pristine.json' describe('Home page', function () { it('albums
can be viewed on home page', function () { cy.get('.album-list-item').should('have.length', albums.length) albums.forEach((album) => { cy.get('.album-list-item h2') .contains(album.title) .parent('.album-list-item') .should('exist') .find('h3') .should('contain', album.artists.join(' - ')) }) }) })
import albums from '../../../server/jazz-albums-test-pristine.json' describe('Home page', function () { it('albums
can be viewed on home page', function () { cy.get('.album-list-item').should('have.length', albums.length) albums.forEach((album) => { cy.get('.album-list-item h2') .contains(album.title) .parent('.album-list-item') .should('exist') .find('h3') .should('contain', album.artists.join(' - ')) }) }) })
import albums from '../../../server/jazz-albums-test-pristine.json' describe('Home page', function () { it('albums
can be viewed on home page', function () { cy.get('.album-list-item').should('have.length', albums.length) albums.forEach((album) => { cy.get('.album-list-item h2') .contains(album.title) .parent('.album-list-item') .should('exist') .find('h3') .should('contain', album.artists.join(' - ')) }) }) })
import albums from '../../../server/jazz-albums-test-pristine.json' describe('Home page', function () { it('albums
can be viewed on home page', function () { cy.get('.album-list-item').should('have.length', albums.length) albums.forEach((album) => { cy.get('.album-list-item h2') .contains(album.title) .parent('.album-list-item') .should('exist') .find('h3') .should('contain', album.artists.join(' - ')) }) }) })
import albums from '../../../server/jazz-albums-test-pristine.json' describe('Home page', function () { it('albums
can be viewed on home page', function () { cy.get('.album-list-item').should('have.length', albums.length) albums.forEach((album) => { cy.get('.album-list-item h2') .contains(album.title) .parent('.album-list-item') .should('exist') .find('h3') .should('contain', album.artists.join(' - ')) }) }) })
import albums from '../../../server/jazz-albums-test-pristine.json' describe('Home page', function () { it('albums
can be viewed on home page', function () { cy.get('.album-list-item').should('have.length', albums.length) albums.forEach((album) => { cy.get('.album-list-item h2') .contains(album.title) .parent('.album-list-item') .should('exist') .find('h3') .should('contain', album.artists.join(' - ')) }) }) })
import albums from '../../../server/jazz-albums-test-pristine.json' describe('Home page', function () { it('albums
can be viewed on home page', function () { cy.get('.album-list-item').should('have.length', albums.length) albums.forEach((album) => { cy.get('.album-list-item h2') .contains(album.title) .parent('.album-list-item') .should('exist') .find('h3') .should('contain', album.artists.join(' - ')) }) }) })
it('sorts albums', function () { function albumsSortedLike(albumList) { albumList.forEach((album, index)
=> { cy.get('.album-list-item') .eq(index) .find('h2') .should('contain', album.title) }) } cy.contains('.filter-section', 'Sort By').find('select').select('Title') albumsSortedLike(albumsSortedByTitle) cy.contains('.filter-section', 'Sort By').find('select').select('Artist') albumsSortedLike(albumsSortedByArtists) cy.contains('.filter-section', 'Sort By').find('select').select('Default') albumsSortedLike(albumsSortedById) })
it('sorts albums', function () { function albumsSortedLike(albumList) { albumList.forEach((album, index)
=> { cy.get('.album-list-item') .eq(index) .find('h2') .should('contain', album.title) }) } cy.contains('.filter-section', 'Sort By').find('select').select('Title') albumsSortedLike(albumsSortedByTitle) cy.contains('.filter-section', 'Sort By').find('select').select('Artist') albumsSortedLike(albumsSortedByArtists) cy.contains('.filter-section', 'Sort By').find('select').select('Default') albumsSortedLike(albumsSortedById) })
it('sorts albums', function () { function albumsSortedLike(albumList) { albumList.forEach((album, index)
=> { cy.get('.album-list-item') .eq(index) .find('h2') .should('contain', album.title) }) } cy.contains('.filter-section', 'Sort By').find('select').select('Title') albumsSortedLike(albumsSortedByTitle) cy.contains('.filter-section', 'Sort By').find('select').select('Artist') albumsSortedLike(albumsSortedByArtists) cy.contains('.filter-section', 'Sort By').find('select').select('Default') albumsSortedLike(albumsSortedById) })
it('sorts albums', function () { function albumsSortedLike(albumList) { albumList.forEach((album, index)
=> { cy.get('.album-list-item') .eq(index) .find('h2') .should('contain', album.title) }) } cy.contains('.filter-section', 'Sort By').find('select').select('Title') albumsSortedLike(albumsSortedByTitle) cy.contains('.filter-section', 'Sort By').find('select').select('Artist') albumsSortedLike(albumsSortedByArtists) cy.contains('.filter-section', 'Sort By').find('select').select('Default') albumsSortedLike(albumsSortedById) })
it('sorts albums', function () { function albumsSortedLike(albumList) { albumList.forEach((album, index)
=> { cy.get('.album-list-item') .eq(index) .find('h2') .should('contain', album.title) }) } cy.contains('.filter-section', 'Sort By').find('select').select('Title') albumsSortedLike(albumsSortedByTitle) cy.contains('.filter-section', 'Sort By').find('select').select('Artist') albumsSortedLike(albumsSortedByArtists) cy.contains('.filter-section', 'Sort By').find('select').select('Default') albumsSortedLike(albumsSortedById) })
it('sorts albums', function () { function albumsSortedLike(albumList) { albumList.forEach((album, index)
=> { cy.get('.album-list-item') .eq(index) .find('h2') .should('contain', album.title) }) } cy.contains('.filter-section', 'Sort By').find('select').select('Title') albumsSortedLike(albumsSortedByTitle) cy.contains('.filter-section', 'Sort By').find('select').select('Artist') albumsSortedLike(albumsSortedByArtists) cy.contains('.filter-section', 'Sort By').find('select').select('Default') albumsSortedLike(albumsSortedById) })
it('sorts albums', function () { function albumsSortedLike(albumList) { albumList.forEach((album, index)
=> { cy.get('.album-list-item') .eq(index) .find('h2') .should('contain', album.title) }) } cy.contains('.filter-section', 'Sort By').find('select').select('Title') albumsSortedLike(albumsSortedByTitle) cy.contains('.filter-section', 'Sort By').find('select').select('Artist') albumsSortedLike(albumsSortedByArtists) cy.contains('.filter-section', 'Sort By').find('select').select('Default') albumsSortedLike(albumsSortedById) })
it('searches for artists', function () { cy.contains('.filter-section', 'Search Artists') .find('input')
.type('John Coltrane') cy.get('.album-list-item') .should('have.length', 2) .find('h2') .should('contain', 'A Love Supreme') .and('contain', 'Blue Train') .parent('.album-list-item') .find('h3') .should('contain', 'John Coltrane') })
it('searches for artists', function () { cy.contains('.filter-section', 'Search Artists') .find('input')
.type('John Coltrane') cy.get('.album-list-item') .should('have.length', 2) .find('h2') .should('contain', 'A Love Supreme') .and('contain', 'Blue Train') .parent('.album-list-item') .find('h3') .should('contain', 'John Coltrane') })
it('searches for artists', function () { cy.contains('.filter-section', 'Search Artists') .find('input')
.type('John Coltrane') cy.get('.album-list-item') .should('have.length', 2) .find('h2') .should('contain', 'A Love Supreme') .and('contain', 'Blue Train') .parent('.album-list-item') .find('h3') .should('contain', 'John Coltrane') })
it('searches for artists', function () { cy.contains('.filter-section', 'Search Artists') .find('input')
.type('John Coltrane') cy.get('.album-list-item') .should('have.length', 2) .find('h2') .should('contain', 'A Love Supreme') .and('contain', 'Blue Train') .parent('.album-list-item') .find('h3') .should('contain', 'John Coltrane') })
it('searches for artists', function () { cy.contains('.filter-section', 'Search Artists') .find('input')
.type('John Coltrane') cy.get('.album-list-item') .should('have.length', 2) .find('h2') .should('contain', 'A Love Supreme') .and('contain', 'Blue Train') .parent('.album-list-item') .find('h3') .should('contain', 'John Coltrane') })
it('searches for artists', function () { cy.contains('.filter-section', 'Search Artists') .find('input')
.type('John Coltrane') cy.get('.album-list-item') .should('have.length', 2) .find('h2') .should('contain', 'A Love Supreme') .and('contain', 'Blue Train') .parent('.album-list-item') .find('h3') .should('contain', 'John Coltrane') })
it('searches for artists', function () { cy.contains('.filter-section', 'Search Artists') .find('input')
.type('John Coltrane') cy.get('.album-list-item') .should('have.length', 2) .find('h2') .should('contain', 'A Love Supreme') .and('contain', 'Blue Train') .parent('.album-list-item') .find('h3') .should('contain', 'John Coltrane') })
Markup or CSS changes?
const AlbumListItem = ({ album }) => ( <Link className="album-list-item"
to={`/albums/${album.id}`} > <img src={album.coverUrl} alt="" /> <RatingIcon rating={album.rating} /> <h2>{album.title}</h2> <h3>{album.artists.join(' - ')}</h3> </Link> )
const AlbumListItem = ({ album }) => ( <Link className="album"
to={`/albums/${album.id}`} > <img src={album.coverUrl} alt="" /> <RatingIcon rating={album.rating} /> <h2>{album.title}</h2> <h3>{album.artists.join(' - ')}</h3> </Link> )
const AlbumListItem = ({ album }) => ( <Link className="album"
to={`/albums/${album.id}`} > <img src={album.coverUrl} alt="" /> <RatingIcon rating={album.rating} /> <h3>{album.title}</h3> <h4>{album.artists.join(' - ')}</h4> </Link> )
None
const AlbumListItem = ({ album }) => ( <Link className="album-list-item"
to={`/albums/${album.id}`} > <img src={album.coverUrl} alt="" /> <RatingIcon rating={album.rating} /> <h2>{album.title}</h2> <h3>{album.artists.join(' - ')}</h3> </Link> )
const AlbumListItemSelectors = ({ album }) => ( <Link className="album-list-item"
to={`/albums/${album.id}`} data-test="album-list-item" > <img src={album.coverUrl} alt="" /> <RatingIcon rating={album.rating} /> <h2 data-test="title">{album.title}</h2> <h3 data-test="artists">{album.artists.join(' - ')}</h3> </Link> )
const AlbumListItemSelectors = ({ album }) => ( <Link className="album-list-item"
to={`/albums/${album.id}`} data-test="album-list-item" > <img src={album.coverUrl} alt="" /> <RatingIcon rating={album.rating} /> <h2 data-test="title">{album.title}</h2> <h3 data-test="artists">{album.artists.join(' - ')}</h3> </Link> )
it('albums can be viewed on home page', function () {
cy.get('[data-test="album-list-item"]') .should('have.length', albums.length) albums.forEach((album) => { cy.get('[data-test="title"]') .contains(album.title) .parent('[data-test="album-list-item"]') .should('exist') .find('[data-test="artists"]') .should('contain', album.artists.join(' - ')) }) })
it('albums can be viewed on home page', function () {
cy.get('[data-test="album-list-item"]') .should('have.length', albums.length) albums.forEach((album) => { cy.get('[data-test="title"]') .contains(album.title) .parent('[data-test="album-list-item"]') .should('exist') .find('[data-test="artists"]') .should('contain', album.artists.join(' - ')) }) })
it('albums can be viewed on home page', function () {
cy.get('[data-test="album-list-item"]') .should('have.length', albums.length) albums.forEach((album) => { cy.get('[data-test="title"]') .contains(album.title) .parent('[data-test="album-list-item"]') .should('exist') .find('[data-test="artists"]') .should('contain', album.artists.join(' - ')) }) })
it('albums can be viewed on home page', function () {
cy.get('[data-test="album-list-item"]') .should('have.length', albums.length) albums.forEach((album) => { cy.get('[data-test="title"]') .contains(album.title) .parent('[data-test="album-list-item"]') .should('exist') .find('[data-test="artists"]') .should('contain', album.artists.join(' - ')) }) })
it('albums can be viewed on home page', function () {
cy.get('[data-test="album-list-item"]') .should('have.length', albums.length) albums.forEach((album) => { cy.get('[data-test="title"]') .contains(album.title) .parent('[data-test="album-list-item"]') .should('exist') .find('[data-test="artists"]') .should('contain', album.artists.join(' - ')) }) })
it('albums can be viewed on home page', function () {
cy.get('[data-test="album-list-item"]') .should('have.length', albums.length) albums.forEach((album) => { cy.contains('[data-test="album-list-item"]', album.title) .should('contain', album.artists.join(' - ')) }) })
<select value={sorter} onChange={(e) => setSorter(e.target.value)} data-test="sort-by" > <option value={Sorter.Id}>Default</option> <option
value={Sorter.Title}>Title</option> <option value={Sorter.Artist}>Artist</option> </select>
cy.get('[data-test="sort-by"]').select('Title') albumsSortedLike(albumsSortedByTitle) cy.get('[data-test="sort-by"]').select('Artist') albumsSortedLike(albumsSortedByArtists) cy.get('[data-test="sort-by"]').select('Default') albumsSortedLike(albumsSortedById)
it('searches for artists', function () { cy.get('[data-test="search-artists"]') .type('John Coltrane') cy.get('[data-test="album-list-item"]')
.should('have.length', 2) .and('contain', 'A Love Supreme') .and('contain', 'Blue Train') .and('contain', 'John Coltrane') })
it('searches for artists', function () { cy.get('[data-test="search-artists"]') .type('John Coltrane') cy.get('[data-test="album-list-item"]')
.should('have.length', 2) .and('contain', 'A Love Supreme') .and('contain', 'Blue Train') .and('contain', 'John Coltrane') })
it('searches for artists', function () { cy.get('[data-test="search-artists"]') .type('John Coltrane') cy.get('[data-test="album-list-item"]')
.should('have.length', 2) .and('contain', 'A Love Supreme') .and('contain', 'Blue Train') .and('contain', 'John Coltrane') })
it('albums can be viewed on home page', function () {
cy.get('[data-test="album-list-item"]') .should('have.length', albums.length) albums.forEach((album) => { cy.get('[data-test="title"]') .contains(album.title) .parent('[data-test="album-list-item"]') .should('exist') .find('[data-test="artists"]') .should('contain', album.artists.join(' - ')) }) })
it('albums can be viewed on home page', function () {
cy.get('[data-test="album-list-item"]') .should('have.length', albums.length) albums.forEach((album) => { cy.get('[data-test="title"]') .contains(album.title) .parent('[data-test="album-list-item"]') .should('exist') .find('[data-test="artists"]') .should('contain', album.artists.join(' - ')) }) })
Page Objects
// cypress/support/pages/home.js export const albums = () => cy.get('[data-test="album-list-item"]') export
const album = (title) => cy.contains('[data-test="album-list-item"]', title) export const sortBy = () => cy.get('[data-test="sort-by"]') export const searchArtists = () => cy.get('[data-test="search-artists"]')
// cypress/support/pages/home.js export const albums = () => cy.get('[data-test="album-list-item"]') export
const album = (title) => cy.contains('[data-test="album-list-item"]', title) export const sortBy = () => cy.get('[data-test="sort-by"]') export const searchArtists = () => cy.get('[data-test="search-artists"]')
// cypress/support/pages/home.js export const albums = () => cy.get('[data-test="album-list-item"]') export
const album = (title) => cy.contains('[data-test="album-list-item"]', title) export const sortBy = () => cy.get('[data-test="sort-by"]') export const searchArtists = () => cy.get('[data-test="search-artists"]')
import * as homePage from '../support/pages/home' it('albums can be viewed
on home page', function () { homePage .albums() .should('have.length', albums.length) albums.forEach((album) => { homePage .album(album.title) .should('exist') .and('contain', album.artists.join(' - ')) }) })
import * as homePage from '../support/pages/home' it('albums can be viewed
on home page', function () { homePage .albums() .should('have.length', albums.length) albums.forEach((album) => { homePage .album(album.title) .should('exist') .and('contain', album.artists.join(' - ')) }) })
import * as homePage from '../support/pages/home' it('albums can be viewed
on home page', function () { homePage .albums() .should('have.length', albums.length) albums.forEach((album) => { homePage .album(album.title) .should('exist') .and('contain', album.artists.join(' - ')) }) })
import * as homePage from '../support/pages/home' it('albums can be viewed
on home page', function () { homePage .albums() .should('have.length', albums.length) albums.forEach((album) => { homePage .album(album.title) .should('exist') .and('contain', album.artists.join(' - ')) }) })
// cypress/support/pages/home.js export const albums = () => cy.get('[data-test="album-list-item"]') export
const album = (title) => cy.contains('[data-test="album-list-item"]', title) export const sortBy = () => cy.get('[data-test="sort-by"]') export const searchArtists = () => cy.get('[data-test="search-artists"]')
// cypress/support/pages/home.js export const albums = () => cy.get('[data-test="album-list-item"]') export
const album = (title) => cy.contains('[data-test="album-list-item"]', title) export const sortBy = () => cy.get('[data-test="sort-by"]') export const searchArtists = () => cy.get('[data-test="search-artists"]')
// cypress/support/pages/home.js export const albums = () => cy.get('[data-test="album-list-item"]') export
const album = (title) => cy.contains('[data-test="album-list-item"]', title) export const sortBy = () => cy.get('[data-test="sort-by"]') export const searchArtists = () => cy.get('[data-test="search-artists"]')
it('sorts albums', function () { function albumsSortedLike(albumList) { albumList.forEach((album, index)
=> { homePage.albums().eq(index).should('contain', album.title) }) } homePage.sortBy().select('Title') albumsSortedLike(albumsSortedByTitle) homePage.sortBy().select('Artist') albumsSortedLike(albumsSortedByArtists) homePage.sortBy().select('Default') albumsSortedLike(albumsSortedById) })
it('sorts albums', function () { function albumsSortedLike(albumList) { albumList.forEach((album, index)
=> { homePage.albums().eq(index).should('contain', album.title) }) } homePage.sortBy().select('Title') albumsSortedLike(albumsSortedByTitle) homePage.sortBy().select('Artist') albumsSortedLike(albumsSortedByArtists) homePage.sortBy().select('Default') albumsSortedLike(albumsSortedById) })
it('sorts albums', function () { function albumsSortedLike(albumList) { albumList.forEach((album, index)
=> { homePage.albums().eq(index).should('contain', album.title) }) } homePage.sortBy().select('Title') albumsSortedLike(albumsSortedByTitle) homePage.sortBy().select('Artist') albumsSortedLike(albumsSortedByArtists) homePage.sortBy().select('Default') albumsSortedLike(albumsSortedById) })
it('searches for artists', function () { homePage.searchArtists().type('John Coltrane') homePage .albums()
.should('have.length', 2) .and('contain', 'A Love Supreme') .and('contain', 'Blue Train') .and('contain', 'John Coltrane') })
it('searches for artists', function () { homePage.searchArtists().type('John Coltrane') homePage .albums()
.should('have.length', 2) .and('contain', 'A Love Supreme') .and('contain', 'Blue Train') .and('contain', 'John Coltrane') })
it('searches for artists', function () { homePage.searchArtists().type('John Coltrane') homePage .albums()
.should('have.length', 2) .and('contain', 'A Love Supreme') .and('contain', 'Blue Train') .and('contain', 'John Coltrane') })
Repetition
export const albums = () => cy.get('[data-test="album-list-item"]') export const album
= (title) => cy.contains('[data-test="album-list-item"]', title) export const sortBy = () => cy.get('[data-test="sort-by"]') export const searchArtists = () => cy.get('[data-test="search-artists"]')
export const albums = () => cy.get('[data-test="album-list-item"]') export const album
= (title) => cy.contains('[data-test="album-list-ite"]', title) export const sortBy = () => cy.get('[data-test=sort-by"]') export const searchArtists = () => cy.get('[data-test="search-artists"')
export const albums = () => cy.get('[data-test="album-list-item"]') export const album
= (title) => cy.contains('[data-test="album-list-ite"]', title) export const sortBy = () => cy.get('[data-test=sort-by"]') export const searchArtists = () => cy.get('[data-test="search-artists"')
export const albums = () => cy.get('[data-test="album-list-item"]') export const album
= (title) => cy.contains('[data-test="album-list-ite"]', title) export const sortBy = () => cy.get('[data-test=sort-by"]') export const searchArtists = () => cy.get('[data-test="search-artists"')
Commands
// cypress/support/commands.js Cypress.Commands.add( 'getByDataTest', (key) => cy.get(`[data-test="${key}"]`) ) // cypress/support/index.js
import './commands'
// cypress/support/commands.js Cypress.Commands.add( 'getByDataTest', (key) => cy.get(`[data-test="${key}"]`) ) // cypress/support/index.js
import './commands'
// cypress/support/commands.js Cypress.Commands.add( 'getByDataTest', (key) => cy.get(`[data-test="${key}"]`) ) // cypress/support/index.js
import './commands'
// cypress/support/commands.js Cypress.Commands.add( 'getByDataTest', (key) => cy.get(`[data-test="${key}"]`) ) // cypress/support/index.js
import './commands'
// cypress/support/commands.js Cypress.Commands.add( 'getByDataTest', (key) => cy.get(`[data-test="${key}"]`) ) // cypress/support/index.js
import './commands'
// cypress/support/commands.js Cypress.Commands.add( 'getByDataTest', (key) => cy.get(`[data-test="${key}"]`) ) // cypress/support/index.js
import './commands'
export const albums = () => cy.get('[data-test="album-list-item"]') export const album
= (title) => cy.contains('[data-test="album-list-item"]', title) export const sortBy = () => cy.get('[data-test="sort-by"]') export const searchArtists = () => cy.get('[data-test="search-artists"]')
export const albums = () => cy.getByDataTest('album-list-item') export const album
= (title) => cy.contains('[data-test="album-list-item"]', title) export const sortBy = () => cy.getByDataTest('sort-by') export const searchArtists = () => cy.getByDataTest('search-artists')
export const albums = () => cy.getByDataTest('album-list-item') export const album
= (title) => cy.contains('[data-test="album-list-item"]', title) export const sortBy = () => cy.getByDataTest('sort-by') export const searchArtists = () => cy.getByDataTest('search-artists')
Cypress.Commands.add( 'containsByDataTest', (key, content) => cy.contains(`[data-test="${key}"]`, content) )
Cypress.Commands.add( 'containsByDataTest', (key, content) => cy.contains(`[data-test="${key}"]`, content) )
export const album = (title) => cy.containsByDataTest('album-list-item', title)
API Timeouts
describe('Album page', function () { // ... it('can be viewed',
function () { albumPage.title().should('contain', this.album.title) albumPage.artists().should('contain', this.album.artists) }) // ... })
describe('Album page', function () { // ... it('can be viewed',
function () { albumPage.title().should('contain', this.album.title) albumPage.artists().should('contain', this.album.artists) }) // ... })
Wait
it('can be viewed', function () { cy.wait(6000) albumPage.title().should('contain', this.album.title) albumPage.artists().should('contain',
this.album.artists) })
it('can be viewed', function () { cy.wait(6000) albumPage.title().should('contain', this.album.title) albumPage.artists().should('contain',
this.album.artists) })
it('can be viewed', function () { cy.wait(6000) albumPage.title().should('contain', this.album.title) albumPage.artists().should('contain',
this.album.artists) })
beforeEach(function () { cy.server() cy.route(buildApiUrl('/albums/*')).as('getAlbum') // ... }) Routes and
Aliases
beforeEach(function () { cy.server() cy.route(buildApiUrl('/albums/*')).as('getAlbum') // ... }) Routes and
Aliases
beforeEach(function () { cy.server() cy.route(buildApiUrl('/albums/*')).as('getAlbum') // ... }) Routes and
Aliases
beforeEach(function () { cy.server() cy.route(buildApiUrl('/albums/*')).as('getAlbum') // ... }) Routes and
Aliases
beforeEach(function () { cy.server() cy.route(buildApiUrl('/albums/*')).as('getAlbum') // ... }) Routes and
Aliases
it('can be viewed', function () { cy.wait('@getAlbum') albumPage.title().should('contain', this.album.title) albumPage.artists().should('contain',
this.album.artists) })
Api Testing
describe('REST API: albums', function () { it('can fetch all albums',
function () { cy.request({ url: buildApiUrl('/albums'), headers: { 'Content-Type': 'application/json', }, auth: { bearer: this.token }, }) .its('body') .should('have.length', this.albums.length) .each((album) => { expect(this.albumTitles).to.include(album.title) }) }) })
describe('REST API: albums', function () { it('can fetch all albums',
function () { cy.request({ url: buildApiUrl('/albums'), headers: { 'Content-Type': 'application/json', }, auth: { bearer: this.token }, }) .its('body') .should('have.length', this.albums.length) .each((album) => { expect(this.albumTitles).to.include(album.title) }) }) })
describe('REST API: albums', function () { it('can fetch all albums',
function () { cy.request({ url: buildApiUrl('/albums'), headers: { 'Content-Type': 'application/json', }, auth: { bearer: this.token }, }) .its('body') .should('have.length', this.albums.length) .each((album) => { expect(this.albumTitles).to.include(album.title) }) }) })
describe('REST API: albums', function () { it('can fetch all albums',
function () { cy.request({ url: buildApiUrl('/albums'), headers: { 'Content-Type': 'application/json', }, auth: { bearer: this.token }, }) .its('body') .should('have.length', this.albums.length) .each((album) => { expect(this.albumTitles).to.include(album.title) }) }) })
describe('REST API: albums', function () { it('can fetch all albums',
function () { cy.request({ url: buildApiUrl('/albums'), headers: { 'Content-Type': 'application/json', }, auth: { bearer: this.token }, }) .its('body') .should('have.length', this.albums.length) .each((album) => { expect(this.albumTitles).to.include(album.title) }) }) })
describe('REST API: albums', function () { it('can fetch all albums',
function () { cy.request({ url: buildApiUrl('/albums'), headers: { 'Content-Type': 'application/json', }, auth: { bearer: this.token }, }) .its('body') .should('have.length', this.albums.length) .each((album) => { expect(this.albumTitles).to.include(album.title) }) }) })
describe('REST API: albums', function () { it('can fetch all albums',
function () { cy.request({ url: buildApiUrl('/albums'), headers: { 'Content-Type': 'application/json', }, auth: { bearer: this.token }, }) .its('body') .should('have.length', this.albums.length) .each((album) => { expect(this.albumTitles).to.include(album.title) }) }) })
describe('REST API: albums', function () { it('can fetch all albums',
function () { cy.request({ url: buildApiUrl('/albums'), headers: { 'Content-Type': 'application/json', }, auth: { bearer: this.token }, }) .its('body') .should('have.length', this.albums.length) .each((album) => { expect(this.albumTitles).to.include(album.title) }) }) })
it('can fetch an album', function () { const album =
this.albums[0] cy.request({ url: buildApiUrl(`/albums/${album.id}`), headers: { 'Content-Type': 'application/json', }, auth: { bearer: this.token }, }) .its('body') .should('containSubset', { id: album.id, title: album.title, artists: album.artists, rating: 'NOT_RATED', userReviews: [], }) })
it('can fetch an album', function () { const album =
this.albums[0] cy.request({ url: buildApiUrl(`/albums/${album.id}`), headers: { 'Content-Type': 'application/json', }, auth: { bearer: this.token }, }) .its('body') .should('containSubset', { id: album.id, title: album.title, artists: album.artists, rating: 'NOT_RATED', userReviews: [], }) })
it('can fetch an album', function () { const album =
this.albums[0] cy.request({ url: buildApiUrl(`/albums/${album.id}`), headers: { 'Content-Type': 'application/json', }, auth: { bearer: this.token }, }) .its('body') .should('containSubset', { // chai-subset plugin id: album.id, title: album.title, artists: album.artists, rating: 'NOT_RATED', userReviews: [], }) })
it('can fetch an album', function () { const album =
this.albums[0] cy.request({ url: buildApiUrl(`/albums/${album.id}`), headers: { 'Content-Type': 'application/json', }, auth: { bearer: this.token }, }) .its('body') .should('containSubset', { id: album.id, title: album.title, artists: album.artists, rating: 'NOT_RATED', userReviews: [], }) })
it('can fetch an album', function () { const album =
this.albums[0] cy.request({ url: buildApiUrl(`/albums/${album.id}`), headers: { 'Content-Type': 'application/json', }, auth: { bearer: this.token }, }) .its('body') .should('containSubset', { id: album.id, title: album.title, artists: album.artists, rating: 'NOT_RATED', userReviews: [], }) })
Cypress.Commands.add('apiRequest', ({ body, url, ...options }) => { const token
= localStorage.getItem('jwt') return cy.request({ ...options, failOnStatusCode: false, url: buildApiUrl(url), headers: { 'Content-Type': 'application/json', }, auth: { bearer: token }, ...(body && { body: JSON.stringify(body) }), }) })
Cypress.Commands.add('apiRequest', ({ body, url, ...options }) => { const token
= localStorage.getItem('jwt') return cy.request({ ...options, failOnStatusCode: false, url: buildApiUrl(url), headers: { 'Content-Type': 'application/json', }, auth: { bearer: token }, ...(body && { body: JSON.stringify(body) }), }) })
Cypress.Commands.add('apiRequest', ({ body, url, ...options }) => { const token
= localStorage.getItem('jwt') return cy.request({ ...options, failOnStatusCode: false, url: buildApiUrl(url), headers: { 'Content-Type': 'application/json', }, auth: { bearer: token }, ...(body && { body: JSON.stringify(body) }), }) })
Cypress.Commands.add('apiRequest', ({ body, url, ...options }) => { const token
= localStorage.getItem('jwt') return cy.request({ ...options, failOnStatusCode: false, url: buildApiUrl(url), headers: { 'Content-Type': 'application/json', }, auth: { bearer: token }, ...(body && { body: JSON.stringify(body) }), }) })
Cypress.Commands.add('apiRequest', ({ body, url, ...options }) => { const token
= localStorage.getItem('jwt') return cy.request({ ...options, failOnStatusCode: false, url: buildApiUrl(url), headers: { 'Content-Type': 'application/json', }, auth: { bearer: token }, ...(body && { body: JSON.stringify(body) }), }) })
Cypress.Commands.add('apiRequest', ({ body, url, ...options }) => { const token
= localStorage.getItem('jwt') return cy.request({ ...options, failOnStatusCode: false, url: buildApiUrl(url), headers: { 'Content-Type': 'application/json', }, auth: { bearer: token }, ...(body && { body: JSON.stringify(body) }), }) })
Cypress.Commands.add('apiRequest', ({ body, url, ...options }) => { const token
= localStorage.getItem('jwt') return cy.request({ ...options, failOnStatusCode: false, url: buildApiUrl(url), headers: { 'Content-Type': 'application/json', }, auth: { bearer: token }, ...(body && { body: JSON.stringify(body) }), }) })
Cypress.Commands.add('apiRequest', ({ body, url, ...options }) => { const token
= localStorage.getItem('jwt') return cy.request({ ...options, failOnStatusCode: false, url: buildApiUrl(url), headers: { 'Content-Type': 'application/json', }, auth: { bearer: token }, ...(body && { body: JSON.stringify(body) }), }) })
Cypress.Commands.add('apiRequest', ({ body, url, ...options }) => { const token
= localStorage.getItem('jwt') return cy.request({ ...options, failOnStatusCode: false, url: buildApiUrl(url), headers: { 'Content-Type': 'application/json', }, auth: { bearer: token }, ...(body && { body: JSON.stringify(body) }), }) })
Cypress.Commands.add('apiRequest', ({ body, url, ...options }) => { const token
= localStorage.getItem('jwt') return cy.request({ ...options, failOnStatusCode: false, url: buildApiUrl(url), headers: { 'Content-Type': 'application/json', }, auth: { bearer: token }, ...(body && { body: JSON.stringify(body) }), }) })
Cypress.Commands.add('apiGet', (url) => cy.apiRequest({ url, method: 'GET' }) ) Cypress.Commands.add('apiPost',
(url, body = null) => cy.apiRequest({ body, url, method: 'POST' }) )
Cypress.Commands.add('apiGet', (url) => cy.apiRequest({ url, method: 'GET' }) ) Cypress.Commands.add('apiPost',
(url, body = null) => cy.apiRequest({ body, url, method: 'POST' }) )
Cypress.Commands.add('apiGet', (url) => cy.apiRequest({ url, method: 'GET' }) ) Cypress.Commands.add('apiPost',
(url, body = null) => cy.apiRequest({ body, url, method: 'POST' }) )
it('can fetch all albums', function () { cy.apiGet('/albums') .its('body') .should('have.length',
this.albums.length) .each((album) => { expect(this.albumTitles).to.include(album.title) }) })
it('can review an album and remove the review', function ()
{ const album = this.albums[0] const review = 'Great album!' const expectedReview = { review, user: { name: 'Emmett Brown', email: '
[email protected]
' }, } cy.apiPost(`/albums/${album.id}/review`, { review }) .its('body') .should('deep.equal', expectedReview) cy.apiGet(`/albums/${album.id}`) .its('body.userReviews') .should('deep.equal', [expectedReview]) cy.apiPost(`/albums/${album.id}/remove-review`) .its('body') .should('deep.equal', { status: 'Removed review' }) cy.apiGet(`/albums/${album.id}`) .its('body.userReviews') .should('deep.equal', []) })
it('can review an album and remove the review', function ()
{ const album = this.albums[0] const review = 'Great album!' const expectedReview = { review, user: { name: 'Emmett Brown', email: '
[email protected]
' }, } cy.apiPost(`/albums/${album.id}/review`, { review }) .its('body') .should('deep.equal', expectedReview) cy.apiGet(`/albums/${album.id}`) .its('body.userReviews') .should('deep.equal', [expectedReview]) cy.apiPost(`/albums/${album.id}/remove-review`) .its('body') .should('deep.equal', { status: 'Removed review' }) cy.apiGet(`/albums/${album.id}`) .its('body.userReviews') .should('deep.equal', []) })
it('can review an album and remove the review', function ()
{ const album = this.albums[0] const review = 'Great album!' const expectedReview = { review, user: { name: 'Emmett Brown', email: '
[email protected]
' }, } cy.apiPost(`/albums/${album.id}/review`, { review }) .its('body') .should('deep.equal', expectedReview) cy.apiGet(`/albums/${album.id}`) .its('body.userReviews') .should('deep.equal', [expectedReview]) cy.apiPost(`/albums/${album.id}/remove-review`) .its('body') .should('deep.equal', { status: 'Removed review' }) cy.apiGet(`/albums/${album.id}`) .its('body.userReviews') .should('deep.equal', []) })
it('can review an album and remove the review', function ()
{ const album = this.albums[0] const review = 'Great album!' const expectedReview = { review, user: { name: 'Emmett Brown', email: '
[email protected]
' }, } cy.apiPost(`/albums/${album.id}/review`, { review }) .its('body') .should('deep.equal', expectedReview) cy.apiGet(`/albums/${album.id}`) .its('body.userReviews') .should('deep.equal', [expectedReview]) cy.apiPost(`/albums/${album.id}/remove-review`) .its('body') .should('deep.equal', { status: 'Removed review' }) cy.apiGet(`/albums/${album.id}`) .its('body.userReviews') .should('deep.equal', []) })
it('can review an album and remove the review', function ()
{ const album = this.albums[0] const review = 'Great album!' const expectedReview = { review, user: { name: 'Emmett Brown', email: '
[email protected]
' }, } cy.apiPost(`/albums/${album.id}/review`, { review }) .its('body') .should('deep.equal', expectedReview) cy.apiGet(`/albums/${album.id}`) .its('body.userReviews') .should('deep.equal', [expectedReview]) cy.apiPost(`/albums/${album.id}/remove-review`) .its('body') .should('deep.equal', { status: 'Removed review' }) cy.apiGet(`/albums/${album.id}`) .its('body.userReviews') .should('deep.equal', []) })
GraphQL
Cypress.Commands.add('graphQL', (query, variables = {}) => cy.apiPost('/graphql', { query, variables
}) )
Cypress.Commands.add('graphQL', (query, variables = {}) => cy.apiPost('/graphql', { query, variables
}) )
Cypress.Commands.add('graphQL', (query, variables = {}) => cy.apiPost('/graphql', { query, variables
}) )
it('can fetch an album', function () { const album =
this.albums[0] const query = ` query Album($id: ID!) { album(id: $id) { title artists rating userReviews { review } } } ` cy.graphQL(query, { id: album.id }) .its('body.data.album') .should('deep.equal', { title: album.title, artists: album.artists, rating: 'NOT_RATED', userReviews: [], }) })
it('can fetch an album', function () { const album =
this.albums[0] const query = ` query Album($id: ID!) { album(id: $id) { title artists rating userReviews { review } } } ` cy.graphQL(query, { id: album.id }) .its('body.data.album') .should('deep.equal', { title: album.title, artists: album.artists, rating: 'NOT_RATED', userReviews: [], }) })
it('can fetch an album', function () { const album =
this.albums[0] const query = ` query Album($id: ID!) { album(id: $id) { title artists rating userReviews { review } } } ` cy.graphQL(query, { id: album.id }) .its('body.data.album') .should('deep.equal', { title: album.title, artists: album.artists, rating: 'NOT_RATED', userReviews: [], }) })
it('can fetch an album', function () { const album =
this.albums[0] const query = ` query Album($id: ID!) { album(id: $id) { title artists rating userReviews { review } } } ` cy.graphQL(query, { id: album.id }) .its('body.data.album') .should('deep.equal', { title: album.title, artists: album.artists, rating: 'NOT_RATED', userReviews: [], }) })
it('can fetch an album', function () { const album =
this.albums[0] const query = ` query Album($id: ID!) { album(id: $id) { title artists rating userReviews { review } } } ` cy.graphQL(query, { id: album.id }) .its('body.data.album') .should('deep.equal', { title: album.title, artists: album.artists, rating: 'NOT_RATED', userReviews: [], }) })
Authentication?
Cypress.Commands.add('login', (email, password) => { cy.visit('/login') loginPage.email().type(email) loginPage.password().type(password) loginPage.logIn().click() })
Cypress.Commands.add('login', (email, password) => { cy.visit('/login') loginPage.email().type(email) loginPage.password().type(password) loginPage.logIn().click() })
Cypress.Commands.add( 'login', ( email = Cypress.env('USER_EMAIL'), password = Cypress.env('USER_PASSWORD') )
=> cy .request('POST', buildApiUrl('/login'), { email, password }) .its('body.token') .then((token) => { localStorage.setItem('jwt', token) }) )
Cypress.Commands.add( 'login', ( email = Cypress.env('USER_EMAIL'), password = Cypress.env('USER_PASSWORD') )
=> cy .request('POST', buildApiUrl('/login'), { email, password }) .its('body.token') .then((token) => { localStorage.setItem('jwt', token) }) )
Cypress.Commands.add( 'login', ( email = Cypress.env('USER_EMAIL'), password = Cypress.env('USER_PASSWORD') )
=> cy .request('POST', buildApiUrl('/login'), { email, password }) .its('body.token') .then((token) => { localStorage.setItem('jwt', token) }) )
Cypress.Commands.add( 'login', ( email = Cypress.env('USER_EMAIL'), password = Cypress.env('USER_PASSWORD') )
=> cy .request('POST', buildApiUrl('/login'), { email, password }) .its('body.token') .then((token) => { localStorage.setItem('jwt', token) }) )
Cypress.Commands.add( 'login', ( email = Cypress.env('USER_EMAIL'), password = Cypress.env('USER_PASSWORD') )
=> cy .request('POST', buildApiUrl('/login'), { email, password }) .its('body.token') .then((token) => { localStorage.setItem('jwt', token) }) )
beforeEach(function () { cy.login() })
Selector Syndrome
Selector Syndrome data-test Attributes and Page Objects
Repetitive Code
Repetitive Code Custom Commands
Timeouts/Arbitrary Waiting
Timeouts/Arbitrary Waiting Routes and Aliases
Untested APIs
Untested APIs cy.request and Commands
Authenticating via UI
Authenticating via UI API, localStorage/cookies, commands, and beforeEach
Thanks! Thanks! Jeremy Fairbank @elpapapollo Slides: bit.ly/ct-act Repo: bit.ly/act-repo