Tips and Tricks for Writing Cypress Tests

Xin Wang
Test Automation Engineer - News UK

 

Agenda

  • Why Cypress?
  • What Cypress can't do?
  • Tips & Tricks
  • Q&A

9 months ago ...


WebdriverIO V5          VS         Cypress

Why Cypress?

  • Great developer experience

  • Fast execution

  • Good documentation

  • Test runs failures can be captured

  • Automatically waits on commands

  • Cross-browser support

multiple tabs

 

visiting multiple domains in single test

 

Sauce Labs

🤔 How to set up Cypress?

  • Create a `cypress.env.json`

  • Export as `CYPRESS_*`

Set up configuration:

  • cypress.json

🤔 How to switch between multiple configuration files?

Set an environment variable within your plugins

Pass in the CLI as `--env`

// cypress/config/cypress.dev.json

{
  "integrationFolder": "cypress/tests/helios-desktop",
  "screenshotsFolder": "cypress/screenshots/desktop"
}
// cypress/config/cypress.staging.json
{
  "baseUrl": "https://www.staging-thesun.co.uk/",
  "integrationFolder": "cypress/tests/staging",
  "screenshotsFolder": "cypress/screenshots/staging",
  "env": {
    "TEST_ARTICLE": "/tech/7257000/animal-crossing-switch"
  }
}
// cypress/config/cypress.mobile.json
{
    "integrationFolder": "cypress/tests/helios-mobile",
    "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Mobile Safari/537.36",
    "screenshotsFolder": "cypress/screenshots/mobile"
}

🤔 How to run a subset
of tests?

Test Filter

Cucumber tags

Create a helper function: TestFilter()

const TestFilter = (definedTags, runTest) => {
  if (Cypress.env('TEST_TAGS')) {
    const tags = Cypress.env('TEST_TAGS').split(',');
    const isFound = definedTags.some(definedTag => tags.includes(definedTag));

    if (isFound) {
      runTest();
    }
  }
};

export default TestFilter;
import TestFilter from "../support/test-filter";

TestFilter(['dev'], () => {
  it('should have the correct feed', () => {
    const teaserBlocks = data.body.blocks.filter(block => block.type === 'teaser-block');
    teaserBlocks.forEach(block => block.teasers.forEach(teaser => expect(teaser.topic.feed).to.equal(collection.feed)));
  });
});

Use it in your test

"cy:run:dev": "CYPRESS_TEST_TAGS=dev cypress run --env configFile=dev",

Export an environment variable when running the test

CYPRESS_*

🤔 How to work with iFrame?

{
  "chromeWebSecurity": false
}

1. Allow cross-origin iFrames

2. Select elements within the iFrame

cy.get

cy.find

cy.wrap

Cypress.Commands.add("clickIframeElement", selector => {
	cy.get("iframe").then($iframe => {
		const doc = $iframe.contents();
		cy.wrap(doc.find(selector))
			.first()
			.click({ force: true });
	});
});

3. Create custom commands

🤔 How to do API Testing with Cypress?

API Testing

Asserting on a request’s

  • body
  • url
  • headers
describe('Topics API', () => {
  let data;

  before(() => {
    cy.request('/topics').then(res => (data = res));
  });

  it('should return JSON data', () => {
    cy.request('/topics')
      .its("headers")
      .its('content-type')
      .should('include', 'application/json');
  })

  it('should have 200 status code and should not be empty', () => {
    expect(data.status).to.equal(200);
    expect(data.body.topics.length).to.be.above(0);
  });

  it('should have various topics added', () => {
    const topics = data.body.topics;
    const testSections = ['Videos', 'Fabulous', 'UK News', 'Scottish News', 'Irish News'];

    testSections.forEach(section => expect(topics.find(topic => topic.name === section)).to.exist);
  });
});

Confidence
critical paths checked

Slow

Seeding data

- Use sparingly
- critical paths
- one test per feature for the happy path

API Testing

Stubbing on a response’s

  • body
  • status
  • headers

 No guarantee your stubbed responses match the actual data the server sends

Fast
Take control
Fake a delay

Cypress.Commands.add('mockConsentRequest', () => {
  cy.server();
  cy.route('**/consent/v2/**/*', {
    consentedToAny: true,
  });
});
Cypress.Commands.add('mockThirdPartyRequests', () => {
  cy.server();  // enable response stubbing
  // route all GET request that have a URL that matches '**/pixel.adsafeprotected.com/**/*'  and force the response to be: []
  cy.route('**/pixel.adsafeprotected.com/**/*', []); 
  cy.route('**/ib.adnxs.com/**/*', []);
  cy.route('POST', '**/ib.adnxs.com/**/*', []); 
  cy.route('POST', '**/r.skimresources.com/*', []);
  cy.route('**/c.amazon-adsystem.com/**/*', []);
  cy.route('**/api.permutive.com/*', []);
  cy.route('POST', '**/api.permutive.com/*', []);
  cy.route('POST', '**/t.skimresources.com/*', []);
  cy.route('**/securepubads.g.doubleclick.net/**/*', []);
  cy.route('POST', '**/siteintercept.qualtrics.com/*', []);
  cy.route('**/pagead2.googlesyndication.com/**/*', []);
  cy.route('**/fastlane.rubiconproject.com/**/*', []);
  cy.route('POST', '**/elb.the-ozone-project.com/**/*', []);
  cy.route('**/manifest.prod.boltdns.net/**/*', []);
});

🤔 How & When to use cy.wait()?

Wait for requests and their responses

Here is an example of aliasing a route and then waiting on it

describe('Bids and ad targeting - Desktop', () => {
  beforeEach(() => {
    cy.server();
    cy.route('**/securepubads.g.doubleclick.net/**/*').as('DFP');
    cy.visit(Cypress.env('TEST_ARTICLE'));
  });

  it('should return bids from different ad providers', () => {
    cy.wait('@DFP').then(() => {
      cy.window().then(win => {
        const adUnitBids = win.pbjs.adUnits.map((adUnit: AdUnit) => adUnit.bids);
        adUnitBids.forEach((bids: []) => expect(bids.length).to.be.greaterThan(0));
      });
    });
  });
});

Waiting on an aliased route has three advantages:
 

1. Tests are less flake

 

  • URL
  • Method
  • Status Code
  • Request Body
  • Request Headers
  • Response Body
  • Response Headers

3. Assert about the underlying XHR object

2. Failure messages are better

cy.get('.nextArrow--visible', {timeout: 10000}).should('be.visible');
cy.wait(10000);
cy.get('.nextArrow--visible').should('be.visible');

wait(ms) vs { timeout: ms }

cy.wait() - Debugging

Default timeout: 4000ms
Modified timeout: 10000ms

🤔 How to handle changing selectors?

CSS in JS

Problem:
Selectors break from development changes to CSS styles or JS behaviour

Solution:

  1. Don’t target elements based on CSS attributes such as: id, class, tag
  2. Don’t target elements that may change their textContent
  3. Add data-* attributes to make it easier to target elements
<IconButton
    data-testid="mute-button"
    tabIndex={-1}
    onClick={() => toggleMute(volume, unMutedVolume, onChange)}
    size={ButtonSize.Small}
    stylePreset={
      volumeControlButtonStylePreset || volumeControlButtonStyleDefault
    }
  >

🤔 How to test audio/video?

Fixture

Load a fixed set of data located in a file

[
  {
    "src": "https://extras.thetimes.co.uk/web/public/2018/world-cup-alexa-breifing/assets/latest-briefing.mp3",
    "imgAlt": "test image 1",
    "title": "title 1",
    "live": false,
    "imgSrc": "https://via.placeholder.com/150",
    "captionSrc": "captions.vtt"
  },
  {
    "src": "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3",
    "imgAlt": "test image 2",
    "title": "title 2",
    "live": false,
    "imgSrc": "https://via.placeholder.com/150",
    "captionSrc": "captions.vtt"
  },
]
cy.get('@podcasts').then(([podcast0]) => {
  cy.get('@player').should(audio => {
    expect(audio.attr('src')).to.equal(podcast[0].src);
  });
});

cy.get('@skipNext')
  .click()
  .then(() => {
    cy.get('@podcasts').then(podcasts => {
      cy.get('@player').should(audio => {
        expect(audio.attr('src')).to.equal(podcasts[1].src);
      });
    });
});

fixtures/podcast.json

cypress test

🤔 How to verify volume set by user?

localStorage

it('should update slider volume when audio player is muted and persisted when reloaded', () => {
    cy.get('[data-testid="mute-button"]')
      .first()
      .click()
      .window()
      .then(win =>
        expect(win.localStorage.getItem('newskit-audioplayer-volume')).to.eq(
          '0',
        ),
      );

    cy.reload().then(win =>
      expect(win.localStorage.getItem('newskit-audioplayer-volume')).to.eq('0'),
    );
    cy.get('@volumeTrack').should('have.attr', 'values', '0');
  });

🤔 Can I use Cypress to do Accessibility Testing?

Accessibility

🤔 How to run tests in parallel with CircleCI?

Run tests in parallel in CircleCI

 

NOT Run tests in parallel

 

4m10s

 

Run tests in parallel

 

1m 54s

 

2m26s

1. Specifying a Job’s Parallelism Level

cypress_dev_e2e_tests:
    working_directory: ~/code/packages/nu-sun-web-e2e-automation
    docker:
      - image: cypress/browsers:node12.16.1-chrome80-ff73
    resource_class: 2xlarge
    parallelism: 4

2. Splitting and running tests

steps:
      - checkout:
          path: ~/code
      - attach_workspace:
          at: ~/
      - run:
          name: Run Dev Helios Desktop E2E Testing
          command: |
            export CYPRESS_NODE_ENV=dev
            export CYPRESS_TEST_TAGS=dev
            export CYPRESS_baseUrl=xxxx
            mv $(circleci tests glob cypress/tests/helios-desktop/**/*.spec.ts | circleci tests split) cypress/tmp/helios-desktop/tests || true	
            [ "$(ls -A cypress/tmp/helios-desktop/tests)" ] && npm run cy:run:dev:parallel
          when: always

See how tests are split in Artifacts

 

Debugging Cypress tests in CircleCI 

- store_artifacts:
     path: cypress/snapshots

🤔 How to run visual tests with Cypress?

Visual Tests

Write tests

 

 

 

 

 


const mobileTestArticles = [
  { name: 'normal article', url: '/fabulous/10939425/weight-loss-new-cookie-diet-instagram-work' },
  { name: 'video article', url: '/tech/7904073/facebook-collections-christmas-wish-list-how' },
  { name: 'boxout article', url: '/money/10940034/energy-firms-compensation-switching-mistakes' },
  { name: 'liveblog article', url: '/sport/football/10926497/man-utd-news-live-transfer-ighalo-messi-willian-koulibaly' },
];

describe('Mobile Article Content', () => {
  mobileTestArticles.forEach(article => {
    it(`should not have visual regression issue on a ${article.name} on mobile view`, () => {
      cy.viewport('iphone-5');
      cy.mockConsentRequest();
      cy.mockThirdPartyRequests();
      cy.visit(article.url);
      cy.hideArticleElements();

      cy.matchImageSnapshot();
    });
  });
});

Run tests

  • Baseline:
    If running tests in CI, create a Docker image to simulate your CI setup, for example, the node and Cypress version. This helps to eliminate visual differences caused by different versions between CI and local machine.
  • Second time: _diff_output folder

 

 

 

 

🤔 Does Cypress support other languages?

Typescript

References:

Questions?

Join our UK Meetup Group...

Cypress.io UK

Community

Copy of Tips and Tricks for Writing Cypress Tests

By Luca Cruciani

Copy of Tips and Tricks for Writing Cypress Tests

  • 1,098