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:
- Don’t target elements based on CSS attributes such as: id, class, tag
- Don’t target elements that may change their textContent
- 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
Tips and Tricks for Writing Cypress Tests
By Xin Wang
Tips and Tricks for Writing Cypress Tests
- 1,381