Four reasons why WebdriverIO is better than Cypress

Four reasons why WebdriverIO is better than Cypress

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:

Up to 4 times faster

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:

cypress_compare.png

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

cypress_compare_parallel.png

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.

Did you find this article valuable?

Support Hugh McCamphill by becoming a sponsor. Any amount is appreciated!