Frontend Application End-to-End Testing
We all know that testing our website or web applications is important in helping us avoid breaking things down the line. It is what makes refactoring, adding new features, or simply fixing bugs so much easier. But are unit or integration tests enough?
End-to-end testing, or E2E testing, is an approach that tries to simulate the flow an end user would take within your application. You can see it as a real-world scenario. It will test a workflow from beginning to end.
Why are they important? Let’s say that even though all of your unit and integration tests pass, creating an order is not working anymore for some reason. End-to-end testing helps you identify and avoid examples like that.
We all know that refactoring a single function, or even a component, might seem easy, and we might be tempted to delete that extra line of code. Still, in the end, some users down the line will complain that they cannot buy your product or update the delivery address. This is the WHY the E2E tests are important and why we should spend some time implementing them. Sometimes we might even get surprised by the bugs we spot while creating the tests.
Briefly, some benefits of the E2E tests are:
- Much faster than running the tests yourself;
- Target multiple platforms/web browsers at the same time;
- Run them on your CI/CD pipeline and avoid pushing a breaking application/website to production;
- Faster iterations without the fear of breaking things;
- Save your company some money with endless hours of debugging after a refactoring;
- And the list goes on.
Some testing libraries, like Cypress and Playwright, offer solutions to run your tests against all major browsers.
All of this will contribute to a consistent user experience since you’re constantly testing workflows and ensuring that changes you commit to your codebase won’t break, and you ship a defective product to your clients.
Frontend testing is there to evaluate if the presentation layer of a website or app is free of bugs or errors. We use them to ensure recent design/feature/performance modifications have not worsened any component of the UI.
It helps us identify performance issues, verify cross-browser and cross-device functionality and check the integration of 3rd party services and their influence on overall UX.
- Unit Testing - low level of testing, i.e., testing functions, methods, a module, etc.
- Visual Regression Testing - validate if the changes made to the presentational components won’t affect the visual appearance of the website/application;
- Cross Browser Testing - this will check if your website/application will work as expected in different browsers, devices, and assistive tools;
- Integration Testing - they will test if two different components or modules work as expected together
- Accessibility Testing - test if as many people can use your application as possible, will check for accessibility issues that you may introduce for people with disabilities;
- Acceptance Testing - kind of test will verify if the application satisfies business requirements.
The E2E tests should be written after your feature is ready, why is that? Well, you are going to test a feature like an end user, so you will need to run a workflow and not test a particular function, like, for example, on unit tests.
First, you will need to install a tool that would allow you to run the tests on an environment similar to the one your end users will be using. For that, you can probably go with Cypress or Playwright (both already mentioned above). Here and here you can find instructions on how to add them to your project.
Now that you have a tool to run your tests, it’s time to write them or record them.
Nowadays, there are browser plugins/extensions that will help you record a flow and transform your actions into code that you can copy and paste to your file, making the necessary changes just to adapt the data you provide on the inputs. A good example of one of these tools is: Deploy Sentinel, they have a Chrome extension and a Firefox add-on as well.
A good thing about using a tool like this is that your non-technical colleagues can record the flow they take that broke the app and send you the code.
You see, tools like Cypress or Playwright are so fast doing interactions with the app that sometimes you miss what’s wrong with your test, and that’s why it’s important to know why they are failing.
Spend some time analyzing the artifacts both tools give, and you will be able to understand and find WHY the test failed. Is it because the element was not on the UI, because it was a network request that failed, or because you have a problem with your component state?
Don’t use always the same input values. Using always the same input values won’t test your application for different values that a user might type on the input. Dynamically generated input values will do this for you. Here, there are also tools that can be used to generate input values on the flight, for example, fakerjs which can be used to generate a lot of random values, from product names to product prices and currencies. Give it a try, and you will be surprised that some cases have escaped you. And those will be caught by the test you have just created.
Taking about artifacts, if you have your CI/CD setup to run your end-to-end tests (that you actually should), don’t forget to save those artifacts in case the tests failed, so you can download them and see what happened.
Try to come up with a flow that a users normally do when using the application or website. That way, you will be able to test the flow that is close to a real use case.
As stated before, use tools to generate random input values, so it’s diverse, and you can catch edge cases.
Don’t repeat steps unnecessarily. Like a step that is already tested in another file, e.g.: to test that a user can place an order and view it on the dashboard, you must first create a product that can be added to the basket and bought. On another test, you are already testing the product page. In this case, before running your test, just create a product targeting your API, so you will have a product that you can use to place on your basket/shopping cart.
It might be worth testing against your real API instead of mocking the request. Sometimes, even when the API tests are in place, and they don’t spot any problem, your E2E tests can spot a little change on the API and fail. That way, you will be able to spot the problem and don’t break the application for your users or avoid deploying a new version of the API.
Another advantage of testing against your real API is that you are simulating the real usage of the product, so that will also be tested.
Don’t write tests for every interaction a user can do in your application or website, just do wider workflows instead that will test a broader range of interactions. Some component behaviors should be tested in unit or component tests.
When your tests start to grow a lot, maybe it’s time to execute tests in parallel, i.e., spin another instance of your test environment and run some of the tests there. That way, your entire test suit will take less time to complete.
The Playwright, for example, does this out of the box. They will run the tests in parallel with several workers' processes that run at the same time. This is time-saving. But bear in mind that, that your tests shouldn’t have dependencies one from another.
Since end-to-end tests will probably be the part of your pipeline that will take longer, you need to plan carefully when you want to run them. For example, you should probably run tests only when the feature is ready for internal testing and QA or when a Pull Request is opened. This way, you avoid running this expensive step on every commit you make and save you/your company some dollars.
You may also want to run your linting or unit tests job first, as they run faster. If the pipeline fails, you probably want to skip your E2E tests.
Another thing you should consider is if you want to run the E2E tests against a running version of the application on a CI environment or against a preview URL closer to the real deal. Here is an example of how to use the preview URL that Vercel creates to run the tests on GitHub actions.
Here is a simple GitHub action to run E2E tests. Other git solutions might have a similar approach, just adapted to their setup.
run-e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- uses: pnpm/action-setup@v2
with:
version: ${{ env.PNPM_VERSION }}
- uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
cache: pnpm
cache-dependency-path: "**/pnpm-lock.yaml"
- uses: actions/cache@v3
id: playwright-cache
with:
path: |
~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: ${{ runner.os }}-playwright-
- run: npx playwright install-deps
if: steps.playwright-cache.outputs.cache-hit != 'true'
- run: pnpm install --frozen-lockfile
- run: pnpm test:integration
env:
MY_NEEDED_ENV_VAR: ${{ my-super-secret }}
- name: Upload artifact when integration tests fail
if: ${{ failure() }}
uses: actions/upload-artifact@v3
with:
path: path/to/test-results/
name: playwright-artifact
retention-days: 1
End-to-end testing should be an essential component of the testing pipeline in your team. In order to prevent errors in the user's experience, plan for it from the beginning and devote the necessary time, energy, and resources.
Your users will thank you for it.
If you have any questions or suggestions, feel free to reach out to our community and share them.