$30 off During Our Annual Pro Sale. View Details »

Connect.Tech 2020: Advanced Cypress Testing

Connect.Tech 2020: Advanced Cypress Testing

Jeremy Fairbank

September 28, 2020
Tweet

More Decks by Jeremy Fairbank

Other Decks in Programming

Transcript

  1. Testing
    Advanced Cypress
    Testing
    Jeremy Fairbank
    @elpapapollo

    View Slide

  2. @testdouble helps improves
    how the world build software.
    testdouble.com

    View Slide

  3. Available in
    print or e-book
    programming-elm.com

    View Slide

  4. Testing anti-patterns

    View Slide

  5. Assumptions

    View Slide

  6. Assumptions

    View Slide

  7. Assumptions
    End-to-End

    View Slide

  8. Assumptions
    End-to-End
    Real API

    View Slide

  9. Assumptions
    End-to-End
    Real API
    Test Data

    View Slide

  10. The App
    bit.ly/act-repo

    View Slide

  11. Selector Syndrome

    View Slide

  12. 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(' - '))
    })
    })
    })

    View Slide

  13. 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(' - '))
    })
    })
    })

    View Slide

  14. 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(' - '))
    })
    })
    })

    View Slide

  15. 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(' - '))
    })
    })
    })

    View Slide

  16. 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(' - '))
    })
    })
    })

    View Slide

  17. 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(' - '))
    })
    })
    })

    View Slide

  18. 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(' - '))
    })
    })
    })

    View Slide

  19. 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)
    })

    View Slide

  20. 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)
    })

    View Slide

  21. 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)
    })

    View Slide

  22. 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)
    })

    View Slide

  23. 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)
    })

    View Slide

  24. 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)
    })

    View Slide

  25. 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)
    })

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  33. Markup or CSS changes?

    View Slide

  34. const AlbumListItem = ({ album }) => (
    className="album-list-item"
    to={`/albums/${album.id}`}
    >


    {album.title}
    {album.artists.join(' - ')}

    )

    View Slide

  35. const AlbumListItem = ({ album }) => (
    className="album"
    to={`/albums/${album.id}`}
    >


    {album.title}
    {album.artists.join(' - ')}

    )

    View Slide

  36. const AlbumListItem = ({ album }) => (
    className="album"
    to={`/albums/${album.id}`}
    >


    {album.title}
    {album.artists.join(' - ')}

    )

    View Slide

  37. View Slide

  38. const AlbumListItem = ({ album }) => (
    className="album-list-item"
    to={`/albums/${album.id}`}
    >


    {album.title}
    {album.artists.join(' - ')}

    )

    View Slide

  39. const AlbumListItemSelectors = ({ album }) => (
    className="album-list-item"
    to={`/albums/${album.id}`}
    data-test="album-list-item"
    >


    {album.title}
    {album.artists.join(' - ')}

    )

    View Slide

  40. const AlbumListItemSelectors = ({ album }) => (
    className="album-list-item"
    to={`/albums/${album.id}`}
    data-test="album-list-item"
    >


    {album.title}
    {album.artists.join(' - ')}

    )

    View Slide

  41. 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(' - '))
    })
    })

    View Slide

  42. 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(' - '))
    })
    })

    View Slide

  43. 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(' - '))
    })
    })

    View Slide

  44. 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(' - '))
    })
    })

    View Slide

  45. 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(' - '))
    })
    })

    View Slide

  46. 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(' - '))
    })
    })

    View Slide

  47. value={sorter}
    onChange={(e) => setSorter(e.target.value)}
    data-test="sort-by"
    >
    Default
    Title
    Artist

    View Slide

  48. 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)

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  52. 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(' - '))
    })
    })

    View Slide

  53. 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(' - '))
    })
    })

    View Slide

  54. Page Objects

    View Slide

  55. // 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"]')

    View Slide

  56. // 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"]')

    View Slide

  57. // 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"]')

    View Slide

  58. 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(' - '))
    })
    })

    View Slide

  59. 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(' - '))
    })
    })

    View Slide

  60. 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(' - '))
    })
    })

    View Slide

  61. 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(' - '))
    })
    })

    View Slide

  62. // 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"]')

    View Slide

  63. // 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"]')

    View Slide

  64. // 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"]')

    View Slide

  65. 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)
    })

    View Slide

  66. 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)
    })

    View Slide

  67. 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)
    })

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  71. Repetition

    View Slide

  72. 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"]')

    View Slide

  73. 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"')

    View Slide

  74. 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"')

    View Slide

  75. 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"')

    View Slide

  76. Commands

    View Slide

  77. // cypress/support/commands.js
    Cypress.Commands.add(
    'getByDataTest',
    (key) => cy.get(`[data-test="${key}"]`)
    )
    // cypress/support/index.js
    import './commands'

    View Slide

  78. // cypress/support/commands.js
    Cypress.Commands.add(
    'getByDataTest',
    (key) => cy.get(`[data-test="${key}"]`)
    )
    // cypress/support/index.js
    import './commands'

    View Slide

  79. // cypress/support/commands.js
    Cypress.Commands.add(
    'getByDataTest',
    (key) => cy.get(`[data-test="${key}"]`)
    )
    // cypress/support/index.js
    import './commands'

    View Slide

  80. // cypress/support/commands.js
    Cypress.Commands.add(
    'getByDataTest',
    (key) => cy.get(`[data-test="${key}"]`)
    )
    // cypress/support/index.js
    import './commands'

    View Slide

  81. // cypress/support/commands.js
    Cypress.Commands.add(
    'getByDataTest',
    (key) => cy.get(`[data-test="${key}"]`)
    )
    // cypress/support/index.js
    import './commands'

    View Slide

  82. // cypress/support/commands.js
    Cypress.Commands.add(
    'getByDataTest',
    (key) => cy.get(`[data-test="${key}"]`)
    )
    // cypress/support/index.js
    import './commands'

    View Slide

  83. 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"]')

    View Slide

  84. 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')

    View Slide

  85. 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')

    View Slide

  86. Cypress.Commands.add(
    'containsByDataTest',
    (key, content) =>
    cy.contains(`[data-test="${key}"]`, content)
    )

    View Slide

  87. Cypress.Commands.add(
    'containsByDataTest',
    (key, content) =>
    cy.contains(`[data-test="${key}"]`, content)
    )

    View Slide

  88. export const album = (title) =>
    cy.containsByDataTest('album-list-item', title)

    View Slide

  89. API Timeouts

    View Slide

  90. describe('Album page', function () {
    // ...
    it('can be viewed', function () {
    albumPage.title().should('contain', this.album.title)
    albumPage.artists().should('contain', this.album.artists)
    })
    // ...
    })

    View Slide

  91. describe('Album page', function () {
    // ...
    it('can be viewed', function () {
    albumPage.title().should('contain', this.album.title)
    albumPage.artists().should('contain', this.album.artists)
    })
    // ...
    })

    View Slide

  92. Wait

    View Slide

  93. it('can be viewed', function () {
    cy.wait(6000)
    albumPage.title().should('contain', this.album.title)
    albumPage.artists().should('contain', this.album.artists)
    })

    View Slide

  94. it('can be viewed', function () {
    cy.wait(6000)
    albumPage.title().should('contain', this.album.title)
    albumPage.artists().should('contain', this.album.artists)
    })

    View Slide

  95. it('can be viewed', function () {
    cy.wait(6000)
    albumPage.title().should('contain', this.album.title)
    albumPage.artists().should('contain', this.album.artists)
    })

    View Slide

  96. beforeEach(function () {
    cy.server()
    cy.route(buildApiUrl('/albums/*')).as('getAlbum')
    // ...
    })
    Routes and Aliases

    View Slide

  97. beforeEach(function () {
    cy.server()
    cy.route(buildApiUrl('/albums/*')).as('getAlbum')
    // ...
    })
    Routes and Aliases

    View Slide

  98. beforeEach(function () {
    cy.server()
    cy.route(buildApiUrl('/albums/*')).as('getAlbum')
    // ...
    })
    Routes and Aliases

    View Slide

  99. beforeEach(function () {
    cy.server()
    cy.route(buildApiUrl('/albums/*')).as('getAlbum')
    // ...
    })
    Routes and Aliases

    View Slide

  100. beforeEach(function () {
    cy.server()
    cy.route(buildApiUrl('/albums/*')).as('getAlbum')
    // ...
    })
    Routes and Aliases

    View Slide

  101. it('can be viewed', function () {
    cy.wait('@getAlbum')
    albumPage.title().should('contain', this.album.title)
    albumPage.artists().should('contain', this.album.artists)
    })

    View Slide

  102. Api Testing

    View Slide

  103. 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)
    })
    })
    })

    View Slide

  104. 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)
    })
    })
    })

    View Slide

  105. 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)
    })
    })
    })

    View Slide

  106. 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)
    })
    })
    })

    View Slide

  107. 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)
    })
    })
    })

    View Slide

  108. 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)
    })
    })
    })

    View Slide

  109. 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)
    })
    })
    })

    View Slide

  110. 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)
    })
    })
    })

    View Slide

  111. 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: [],
    })
    })

    View Slide

  112. 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: [],
    })
    })

    View Slide

  113. 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: [],
    })
    })

    View Slide

  114. 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: [],
    })
    })

    View Slide

  115. 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: [],
    })
    })

    View Slide

  116. 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) }),
    })
    })

    View Slide

  117. 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) }),
    })
    })

    View Slide

  118. 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) }),
    })
    })

    View Slide

  119. 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) }),
    })
    })

    View Slide

  120. 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) }),
    })
    })

    View Slide

  121. 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) }),
    })
    })

    View Slide

  122. 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) }),
    })
    })

    View Slide

  123. 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) }),
    })
    })

    View Slide

  124. 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) }),
    })
    })

    View Slide

  125. 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) }),
    })
    })

    View Slide

  126. Cypress.Commands.add('apiGet', (url) =>
    cy.apiRequest({ url, method: 'GET' })
    )
    Cypress.Commands.add('apiPost', (url, body = null) =>
    cy.apiRequest({ body, url, method: 'POST' })
    )

    View Slide

  127. Cypress.Commands.add('apiGet', (url) =>
    cy.apiRequest({ url, method: 'GET' })
    )
    Cypress.Commands.add('apiPost', (url, body = null) =>
    cy.apiRequest({ body, url, method: 'POST' })
    )

    View Slide

  128. Cypress.Commands.add('apiGet', (url) =>
    cy.apiRequest({ url, method: 'GET' })
    )
    Cypress.Commands.add('apiPost', (url, body = null) =>
    cy.apiRequest({ body, url, method: 'POST' })
    )

    View Slide

  129. 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)
    })
    })

    View Slide

  130. 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', [])
    })

    View Slide

  131. 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', [])
    })

    View Slide

  132. 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', [])
    })

    View Slide

  133. 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', [])
    })

    View Slide

  134. 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', [])
    })

    View Slide

  135. GraphQL

    View Slide

  136. Cypress.Commands.add('graphQL', (query, variables = {}) =>
    cy.apiPost('/graphql', { query, variables })
    )

    View Slide

  137. Cypress.Commands.add('graphQL', (query, variables = {}) =>
    cy.apiPost('/graphql', { query, variables })
    )

    View Slide

  138. Cypress.Commands.add('graphQL', (query, variables = {}) =>
    cy.apiPost('/graphql', { query, variables })
    )

    View Slide

  139. 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: [],
    })
    })

    View Slide

  140. 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: [],
    })
    })

    View Slide

  141. 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: [],
    })
    })

    View Slide

  142. 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: [],
    })
    })

    View Slide

  143. 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: [],
    })
    })

    View Slide

  144. Authentication?

    View Slide

  145. Cypress.Commands.add('login', (email, password) => {
    cy.visit('/login')
    loginPage.email().type(email)
    loginPage.password().type(password)
    loginPage.logIn().click()
    })

    View Slide

  146. Cypress.Commands.add('login', (email, password) => {
    cy.visit('/login')
    loginPage.email().type(email)
    loginPage.password().type(password)
    loginPage.logIn().click()
    })

    View Slide

  147. 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)
    })
    )

    View Slide

  148. 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)
    })
    )

    View Slide

  149. 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)
    })
    )

    View Slide

  150. 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)
    })
    )

    View Slide

  151. 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)
    })
    )

    View Slide

  152. beforeEach(function () {
    cy.login()
    })

    View Slide

  153. Selector Syndrome

    View Slide

  154. Selector Syndrome
    data-test Attributes
    and Page Objects

    View Slide

  155. Repetitive Code

    View Slide

  156. Repetitive Code
    Custom Commands

    View Slide

  157. Timeouts/Arbitrary Waiting

    View Slide

  158. Timeouts/Arbitrary Waiting
    Routes and Aliases

    View Slide

  159. Untested APIs

    View Slide

  160. Untested APIs
    cy.request and
    Commands

    View Slide

  161. Authenticating via UI

    View Slide

  162. Authenticating via UI
    API, localStorage/cookies,
    commands, and
    beforeEach

    View Slide

  163. Thanks!
    Thanks!
    Jeremy Fairbank
    @elpapapollo
    Slides: bit.ly/ct-act
    Repo: bit.ly/act-repo

    View Slide