Having recently seen Alister Scott's reasons for preferring Playwright over Cypress, I thought it would be nice to do a comparison with WebdriverIO. I've never used Cypress in anger, but I certainly identify with the challenges it has for performing end-to-end test automation.
You may see some similar reasons
Reason 1: WebdriverIO is so much faster than Cypress
Reusing some of Alisters' blog:
This is the simple example taken from Cypress' docs:
describe('My First Test', () => {
it('clicking "type" shows the right headings', () => {
cy.visit('https://example.cypress.io')
cy.contains('type').click()
// Should be on a new URL which includes '/commands/actions'
cy.url().should('include', '/commands/actions')
// Get an input, type into it and verify that the value has been updated
cy.get('.action-email')
.type('fake@email.com')
.should('have.value', 'fake@email.com')
})
})
And as per Alister, this is how long it takes to run locally:
Takes 8 seconds running on my M1 Macbook Air
This is the same test written with WebdriverIO:
describe('My First Test', () => {
it('clicking "type" shows the right headings', async () => {
await browser.url('https://example.cypress.io');
await $('=type').click();
// Should be on a new URL which includes '/commands/actions'
await expect(browser).toHaveUrlContaining('/commands/actions');
// Get an input, type into it and verify that the value has been updated
await $('.action-email').setValue('fake@email.com');
await expect($('.action-email')).toHaveValue('fake@email.com');
})
})
And how long it takes to run:
1.8 seconds, so yeah 4 times faster as well
Reason 2: WebdriverIO has first-class support for parallel running of tests on a single machine both locally and on CI without a subscription or account required
Currently, we run our WebdriverIO tests in CircleCI using Docker Medium Executor, with a max instance of 4.
Reason 3: WebdriverIO supports parallel tests within a single test file – to run Cypress tests in parallel you need to split them across files.
At the time of writing, there is no first-class support for running parallel tests within a single test file for WebdriverIO. There is a project here in the WebdriverIO backlog and service from Wim Selles with some limited support for it.
That said, for the given example, running the tests in one file is still super quick, so probably need a more representative example
And to run them on the same machine I ran these 8 scenarios in Playwright in 6 seconds takes 45 seconds! 7.5 times slower!
Reason 4: WebdriverIO fully supports async/await syntax for clean, readable code.
As discussed by Alister, here is the Cypress example:
describe('My First Test', () => {
it('clicking "type" shows the right headings', () => {
cy.visit('https://example.cypress.io')
cy.contains('type').click()
// Should be on a new URL which includes '/commands/actions'
cy.url().should('include', '/commands/actions')
// Get an input, type into it and verify that the value has been updated
cy.get('.action-email')
.type('fake@email.com')
.should('have.value', 'fake@email.com')
})
})
Note how the function calls chain together – in this simple example it’s not too bad but it quickly gets out of hand. Also, note there’s no indication that this is synchronous code – no await on any calls.
While it is still possible to use WebdriverIO in Sync Mode, its use is discouraged, so here is the test in WebdriverIO, using async / await throughout.
describe('My First Test', () => {
it('clicking "type" shows the right headings', async () => {
await browser.url('https://example.cypress.io');
await $('=type').click();
// Should be on a new URL which includes '/commands/actions'
await expect(browser).toHaveUrlContaining('/commands/actions');
// Get an input, type into it and verify that the value has been updated
await $('.action-email').setValue('fake@email.com');
await expect($('.action-email')).toHaveValue('fake@email.com');
})
})
Reason 5: WebdriverIO doesn’t need plugins (for basic interactions)
WebdriverIO features a huge variety of community plugins allow you to easily integrate and extend your setup to fulfill your requirements, but this is typically for extending capabilities beyond running tests locally (eg running remotely). There is full native support for interactions, as you would expect for a framework implementing the W3C Webdriver Protocol.
There’s so many limitations in Cypress there’s pretty much a plugin for everything. And there’s still so may trade-offs that can’t be be fixed by a plugin.
Need to send the tab key to the browser? There’s a plugin for that. And it doesn’t work well at all according to the comments on the Github issue for lack of tab key support open since 2016 (and still open since I last wrote about Cypress).
Need to upload a file? There’s also a plugin for that.
Need to run tests n-times in a row to measure repeatability? There’s a plugin for that.
To this I would add:
- Need to fill in Stripe? There's a plugin for that.
- Need to interact with an iframe? There's a module for that.
Summary
I'm definitely biased towards a framework that offers native control of the browser and is not sandboxed, and with the development of protocols like Webdriver BiDi, we can use frameworks based around WebDriver that will continue to be compatible with how people need to write tests to deal with modern applications, in a modern JS syntax.