Mastering UI Testing

Mastering UI Testing

Writing UI tests for the web is hard, a lot of times the tests are brittle and unmaintainable. You must run them multiple times and, sometime, you need to exclude some of them from your pipeline.

Β 

I'm going to explain to you the most important best practices that allow you to master the UI tests hell. I'm going to show you how much a tool like Cypress can help you writing and maintaining your UI tests.

I'm Stefano Magni (@NoriSte)

I'm a passionate front-end developer, a testing & automation lover.


I work for Conio, a bitcoin startup based in Milan.


Here you can find my recent open-source contributions.

Slides info

I developed a super simple companion repository for these slides. If you want a follow-up project to play with, check it out. It's a simple authentication.

Β 

https://github.com/NoriSte/working-software-mastering-ui-testing

Slides info

There are a lot of linksΒ into the slides πŸ”—.

Almost all the logos are links too πŸ”—.

Slides are organized both horizontally and vertically (see the bottom-right arrows).

Best practices are highlighted

My two UI testing principles:

β€’ tests must not fail if the app is working (false negatives)

Β 

β€’ if they fail, they must drive me directly to problem

Why is E2E testing hard?

Let's start:

Why is E2E testing hard?

Because we need to test four kind of contracts at the same time!

β€’ HTML - πŸ“„ semantic markup
β€’ CSSΒ  - πŸ‘ visual regressions
β€’ JSΒ  Β - πŸ•Ή interaction flows
β€’ APIΒ  - πŸ” client/server communication

Β 

Why is E2E testing hard?

Because it needs a real browser and a real context!

β€’ βŒ›οΈ everything is asynchronous
β€’ πŸ“¦ E2E tests need a lot of reliable data
β€’ 🐞 debugging E2E tests is hard
β€’ 😭 the browser is slow, the UI is slow, the network is slow

Β 

Why is E2E testing hard?

Because generic browser automation tools s**k!

β€’ ⌨️ no UI
β€’ πŸ”’ no step-by-step test utilities
β€’ πŸš€ no integrated utilities
β€’ πŸ€” no clear reasons for failures

Β 

How could we make E2E testing easier?

First of all:

😱😱😱

Full E2E tests are not

so important!

UI integration tests (with a stubbed server) are way much more reliable!

Suffers from Full E2E test UI integration test
the network Yes πŸ˜“ No πŸŽ‰
the server Yes πŸ˜“ No πŸŽ‰
data resetting Yes πŸ˜“ No πŸŽ‰
complex case data Yes πŸ˜“ No πŸŽ‰

Full E2E tests are not so important!

Full E2E test UI integration test
confidence 100% πŸ’ͺ 90% πŸ€·β€β™‚οΈ
performance* ~20"/test πŸ˜“ ~2.5"/test πŸš€

* benchmarking the Conio's back office test suite (env: staging)

UI integration tests (with a stubbed server) are way much faster!

Full E2E tests are not so important!

⬆️ E2E tests vs UI Integration tests ⬇️

Write a few E2E tests and a lot of UI integration tests

πŸ€™

// Cypress example
cy.server();
cy.route({
  url: "/auth/token",
  method: "POST",
  response: "fixture:authentication-success.json"
})

cy.visit("/login")
cy.get(".username").type("stefano@conio.com")
cy.get(".password").type("mysupersecretpassword")
cy.get(".btn-primary").click()

// the front-end app is going to receive the content
// of the authentication-success.json fixture

Stubbing the back-end is easy with Cypress.

Here an authentication form example.

cy.route({
  url: "/auth/token",
  method: "POST",
  response: "fixture:unexisting-user.json"
})

cy.route({
  url: "/auth/token",
  method: "POST",
  response: "fixture:already-registered-user.json"
})

Stubbing the back-end allows us to check every user flow with ease.

cy.route({
  url: "/auth/token",
  method: "POST",
  response: {},
  status: 401
})

cy.route({
  url: "/auth/token",
  method: "POST",
  response: {},
  status: 500
})

And write an E2E test just for the happy path*.

cy.visit("/login")
cy.get(".username").type("stefano@conio.com")
cy.get(".password").type("mysupersecretpassword")
cy.get(".btn-primary").click()

* the Conio's back office test suite is composed by 80 UI integration tests and 12 E2E tests

πŸ€”

What about the

client/server contract?

Test every request and response payload

πŸ”

How many times the front-end app doesn't work because of bad/misaligned payloads?

cy.server();
cy.route({
  url: "/auth/token",
  method: "POST",
  response: "fixture:authentication-success.json"
}).as("login-request")

cy.visit("/login")
cy.get(".username").type("stefano@conio.com")
cy.get(".password").type("mysupersecretpassword")
cy.get(".btn-primary").click()

cy.wait("@login-request").then(xhr => {
  expect(xhr.request.body).to.have.property("username", "stefano@conio.com")
  expect(xhr.request.body).to.have.property("password", "mysupersecretpassword")
})

Check the request payloads with the UI integration tests.

cy.server();
cy.route({
  url: "/auth/token",
  method: "POST"
}).as("login-request")

cy.visit("/login")
cy.get(".username").type("stefano@conio.com")
cy.get(".password").type("mysupersecretpassword")
cy.get(".btn-primary").click()

cy.wait("@login-request").then(xhr => {
  expect(xhr.status).to.equal(200)
  expect(xhr.response.body).to.have.property("access_token")
  expect(xhr.response.body).to.have.property("refresh_token")
})

Check the response payloads with the E2E tests.

Export/check every back-end schema/elasticsearch mapping/typescript definitions.

Β 

You can use Postman, GIT, my NPM package url-content-changes-checker, Jest snapshot testing.

My url-content-changes-checkerΒ in action

It highlights the changes in a text document

Do not slow down your tests with unnecessary sleeping

πŸ˜΄βŒ›οΈ

Always wait for some determinist events, never make your tests sleeping.

How can I define the deterministic events?

Some examples are:

β€’ every XHR requests

β€’ UI element appearings

β€’ everything valuable for the business

And how can I avoid test sleepings?

Cypress automatically waits!

// Cypress waits up to 60 seconds for a page load
cy.visit("/login")

// up to 4 seconds for an element appearing
cy.get(".username").type("stefano@conio.com")

// up to 5 seconds for an XHR request and
// up to 30 seconds for the XHR response
cy.wait("@login-request")

// and if cypress cannot wait for something, you can use my own plugin
cy.waitUntil(() => cy.getCookie("token")
  .then(cookie => Boolean(cookie && cookie.value)))

Cypress automatically waits!

Expose an authentication shortcut from the front-end

πŸŽπŸ’¨

All the tests need authentication, do not authenticate through the UI all the times.

// front-end
if(window.Cypress) {
  window.conioApp.store = reduxStore
}

// Cypress
window.conioApp.store.dispatch({
  type: "LOGIN_REQUEST",
  "stafeno@conio.com",
  "mysupersecretpassword"
})

Consuming some shortcuts instead of the UI:

β€’ is blazing fast

β€’ avoids you to duplicate the authentication management

⬆️ through UI vs through shortcuts ⬇️

πŸ’ͺ

Expose some constants from the front-end

A lot of times some tests fail for little to insignificant front-end changes.

// instead of...
cy.visit("/login")

// you can
import {LOGIN_PATH} from "@/navigation/paths";
cy.visit(LOGIN_PATH)

// and if "/login" becomes "/sign-in" you will not
// face an annoying test failure

We all hate false negative test failures. Work to reduce them as much as possible.

πŸ™‹β€β™‚οΈπŸ™‹β€β™€οΈ

Assert frequently

The more you assert, the more the failures drive you to the exact problem.

// Cypress example
cy.server();
cy.route("POST", "/auth/token").as("login-request");

cy.visit(LOGIN_PATH)
cy.get(".username").type("stefano@conio.com")
cy.get(".password").type("mysupersecretpassword")
cy.get(".btn-primary").click()

cy.wait("@route_login").then(xhr => {
  expect(xhr.request.body).to.have.property("username", "stefano@conio.com")
  expect(xhr.request.body).to.have.property("password", "mysupersecretpassword")
  expect(xhr.status).to.equal(200)
  expect(xhr.response.body).to.have.property("access_token")
  expect(xhr.response.body).to.have.property("refresh_token")
});

cy.get("#success-feedback").should("exist")
cy.url().should("not.include", LOGIN_PATH)

🏎 πŸš™ 🚜

Split the different kind of tests

Different kind of tests have different purposes, usually you need to launch just some of them.

{
  "cy:test:integration": "cypress run --spec \"cypress/**/*.integration.*\"",
  "cy:test:e2e": "cypress run --spec \"cypress/**/*.e2e.*\""
}

My three types of Cypress tests

Type Filename When
integration tests <name>.integration.test.js β€’ development
β€’ CI pipeline
e2e tests <name>.e2e.test.js β€’ post CI pipeline
β€’ cron
monitoring tests <name>.monitoring.test.js β€’ cron

πŸ‘

Test the front-end the same way the user consumes it

The user does not care about selector, he cares about contents.

// instead of testing the front-end through selectors...
cy.get(".username").type("stefano@conio.com")
cy.get(".password").type("mysupersecretpassword")
cy.get(".btn-primary").click()
cy.get("#success-feedback").should("exist")

// ... we can test it though contents, like the user does
import {
  USERNAME,
  PASSWORD,
  LOGIN,
  SUCCESS_FEEDBACK
} from "@/components/LoginForm/strings"

cy.getByText(USERNAME).type("stefano@conio.com")
cy.getByText(PASSWORD).type("mysupersecretpassword")
cy.getByText(LOGIN).click()
cy.getByText(SUCCESS_FEEDBACK).should("exist")

It's made easy by some plugins like cypress-testing-library or cypress-xpath.

More: if your tests consume contents (and not CSS classes selectors, IDs etc.)...

... You just need to take a look at a screenshot to understand why a test failed!

If you can not do that, use ARIA or data-testid attributes.

πŸ€”

Why did I mention TDD?

Because Cypress is perfect to be used as your main development tool and browser.

You can write your acceptance test and apply an outside-in approach to your front-end app.

Cypress can be launched:

# headlessly
$ cypress run

# with its UI
$ cypress open

Cypress can be launched with your Chrome version of choice:

Cypress creates a dedicated, persistent, Chrome user. You can install your DevTools of choice:

The Test Runner is amazing:

The Test Runner allows time-travelling:

Error reporting is amazing:

skip-and-only-uiΒ and cypress-watch-and-reloadΒ plugins are useful too:

Cypress Dashboard is perfect for CI pipelines:

β€’ Tests are re-launched on every CTRL+S

β€’ if a test fails, Cypress automatically save screenshots and videos

β€’ clock (setTimeout) control

β€’ they're working on crossbrowser support

Something more?

πŸŽ‰

So you do not have to test your front-end manually anymore!

Some common questions:

😊

Does Cypress supports only Chrome?

They are working on cross-browser support but yes, only Chrome is supported until now!

Uhm πŸ€”

Cross-browser tests are more useful for visual regressions, integrate AppliTools with Cypress and you get it πŸ˜‰

Is Cypress free?

Yes, you pay only if you want to have it upload a lot of test videos on its servers. πŸ˜‰

Is it all about front-end testing?

πŸ€”

Cypress solves the biggest testing problems but: no, there are other kind of tests.

β€’ JS unit testing

β€’ Component testing

Β  Β β€’ snapshot testing (HTML)

Β  Β β€’ regression testing (CSS)

Β  Β β€’ callback testing

β€’ App visual testing (CSS)

For component development and tests: take a look at Storybook and its storyshots addon.

We are preparing a two-day workshop.

I and Jaga SantagostinoΒ are preparing a two-day workshop about everything related to JavaScript testing.

Β 

The first one will be about React testing.

Β 

Drop us a line if you want to know more about it πŸ—£

I'm working on a "UI Testing Best Practices" book on GitHub.

I've just begun writing it, all contributions are welcome πŸ™Œ

Recommended courses

Recommended sources

We're hiring a front-end developer!

Thank you!

Made with Slides.com