UI Testing Best Practices (4 Part Series)
I'm working on a big UI Testing Best Practices project on GitHub, I share this post to spread it and have direct feedback.
- Component tests
- UI Integration tests
- End-to-end (E2E) tests
The unit tests of a UI, they test every single component in an isolated
Developing components in isolation is important because it allows you to isolate them from the corresponding container/use. A component exists to isolate a single behavior/content (the Single responsibility principle) and therefore, coding it in isolation is profitable.
There are many ways and tools to develop components in isolation but
Storybook became the standard choice because of its effectiveness and its ecosystem.
Components have three types of contracts: the generated output (HTML), their visual aspect (CSS), and the external APIs (props and callbacks).
Testing every aspect could be cumbersome, that's where Storyshots comes in
handy. It allows you to automate:
• the snapshot tests: a snapshot is an output generated by your component, a string containing all the rendered HTML. If the generated HTML changes,
accidentally or not, the snapshot tests fail and you can choose if the changes were intentional or not.
• the visual regression tests: the visual aspect of the component compared pixel by pixel with the previous one, again, you are prompted to choose if you accept the changes or not.
These tests are launched by Storyshots automatically on every Storybook page (AKA "stories").
• the callback tests: a small React container app renders the component passing it some callbacks. Then, the user interactions are simulated and passed the callback is expected to be called.
React Testing Library is the standard library of choice for this kind of tests
• the interaction/state tests: some interactions with a component expect correct state management. This kind of test must be written from the consumer point of view, not from the inner one (ex. the value of the input field when the user fills it, not the inner component state). An interaction/state test should assert the input field value after the keyboard events triggered.
They run the whole app in a real browser without hitting a real server.
These tests are the ace in the hole of every front-end developer. They are blazing fast and less exposed to random failures or false negatives. Cypress is perfect for UI Integration tests.
The front-end application does not know that there is not a real server: every AJAX call is resolved in no time by the testing tool. Static JSON responses (called "fixtures") are used to simulate the server responses. Fixtures allow us to test the front-end state simulating every possible back-end state.
Another interesting effect: Fixtures allow you to work without a working back-end application. You can think about UI Integration tests as "front-end-only tests".
At the core of the most successful test suites, there is a lot of UI Integration tests, considering the best type of test for your front-end app.
They run the whole app interacting with the real server. From the user
interactions (one of the "end") to the business data (the other "end"): everything must work as designed. E2E tests are typically slow because
• they need a working back-end application, typically launched alongside the front-end application. You can't launch them without a server, so you depend on the back-end developers to work
• they need reliable data, seeding and cleaning it before every test
That's why E2E tests are not feasible to be used as the only/main test type. They are pretty important because they are testing everything (front-end + back-end) but they must be used carefully to avoid brittle and hour-long test suites.
In a complete suite with a lot of UI Integration tests, you can think about E2E tests as "back-end tests". What flows should you test through them?
• the Happy Path flows: you need to be sure that, at least, the users are able to complete the basic operations
• everything valuable for your business: happy path or not, test whatever your business cares about (prioritizing them, obviously)
• everything that breaks often: weak areas of the system must be monitored too
Identifying/defining the type of test is useful to group them, to limit their scope, and to choose where to run them or not though the whole application and deployment pipelines.
Again, Cypress is my tool of choice for E2E tests.
You can write a lot of different UI tests and it's a good habit to have a common way of naming the test files.
It's useful because often you need to run just a type of tests, the situations could be:
- during the development process, you need to run just some of them
- you're changing some related components and you need to check that the generated markup does not change
- you're changing a global CSS rule and you need to run only the visual tests
- you're changing an app flow and you need to run the whole app integration tests
- your DevOps colleague needs to be sure that everything is up and running and the easiest way to do that is launching just the E2E tests
- your building pipeline needs to run just the integration and E2E tests
- your monitoring pipeline needs a script to launch the E2E and monitoring tests
If you name your tests wisely, it will be really easy to launch just some kind of them.
cypress run --spec \"cypress/integration/**/*.e2e.*\"
A global way to name the test files does not exist, a suggestion could be to name them with:
- the subject you are testing
- the kind of test (
- the test suffix of choice (
- the file extension (
all of them separated by a period.
Some examples could be