Cypress has a setting to allow tests to retry.
A test is 'retryable' if it can fail once, then pass on a subsequent retry.
Tests are often written without retryability in mind.
If retries are enabled globally in a Cypress project and a test is not retryable, then it will increase the time it takes for a failing test run to actually fail.
Consider this test block:
describe('The user', () => {
before(() => {
cy.visit('/');
});
it('can log in', () => {
cy.get('.user').type('asdf');
cy.get('.pass').type('secret');
cy.get('.login-submit').click();
cy.get('.greeting').should('contain', 'Hello, asdf!');
});
it('can access user settings', () => {
cy.get('a.account-settings').click();
cy.get('.settings .avatar').should('be.visible');
});
it('can access direct messages', () => {
// we stage the test using the back command
cy.go('back');
cy.get('a.messages').click();
// What if the next line is flaky?
cy.get('.unread-messages').should('contain', 'testing');
});
});
If the last line is flaky, maybe it fails.
But, we think, it's ok because the test will just retry.
Right?
Not exactly…
The test is relying upon the page being in a given state at the start of the it
block.
That is, we are relying upon the previous block clicking on a.account-settings
before the last test is run.
Since the test uses cy.go('back')
, we have permanently altered the starting state of the test during the test itself.
So, even if we retry, we will try to go back for a 2nd time, and it will fail.
What about that before
hook (aka beforeAll
)?
Won't it be able to help us during the retry?
Actually, if a test fails, Cypress will do the following:
it
blockbeforeEach/afterEach
hooks (if they exist)beforeEach
hook with the exact state of the page after the failed it
blockit
blockafterEach
hookSo, before
and after
hooks are actually completely excluded from the retry cycle.
So, we have run into a bit of a dilemma: speed vs flake.
Should we set up a known state before each and every test so we can retry?
Or should we just accept flake and failures and have fast test runs?
I have taken to testing every new spec with a helper function that I call failOnce
registered as an afterEach
hook.
let shouldPass = false;
// This function will alter between fail / pass, starting with fail.
const failOnce = () => {
// if you were to use `expect` here it would not work correctly
// since execution will stop after `expect` fails
cy.wrap(shouldPass, { timeout: 0 }).should('be.true');
shouldPass = !shouldPass;
};
The failOnce
function will start with shouldPass = false
and fail the test in the afterEach
hook.
Then, the test will run again with shouldPass = true
, and if the test was written to be retryable, the test will pass.
describe('cypress', () => {
afterEach(failOnce);
it('can retry a test', { retries: 2 }, () => {
cy.wrap('foo').should('equal', 'foo');
});
});
The test can fail if:
If you have the ability to snapshot and restore the application state by modifying cookies/localStorage/applicationStorage, then there are two approaches:
before
and restore in a beforeEach
to a single known state (seems ideal)before
and then afterEach
and restore in beforeEach
depending on desired behavior / DOM manipulation in testsbeforeEach -> it -> afterEach
executes during a retry.afterEach(failOnce)
.