Test driven development (TDD) is a tried and tested way of working in software development, which focuses on writing tests for a desired behaviour, writing just enough code to make the test pass and then looking for any improvements (refactoring) that can be made to your code before repeating this process. Working with TDD has many benefits such as being able to have confidence that your code behaves the way you expect, being able to change and add to your code and know you haven’t introduced any regressions and with a focus on refactoring it should help improve the code quality of your project.
However, TDD has traditionally been used in backend development with Object-oriented Programming, so its principles may seem difficult to apply to front end development. Though its application may not be exactly the same as it is in backend development, TDD can still be used when developing in the frontend to gain the benefits it provides. However, as with TDD in the backend, you have to consider what kind of tests are worth writing.
What kind of tests should be written?
When writing a test you should consider:
- Is it clear what I am trying to test? Could it be tested in a different way?
- What is the essence of what I’m trying to test? Does my test really verify that behaviour?
- How easy is it to write the test?
- How brittle will the test be? How easy is it to maintain it?
- How much value does the test provide me?
- Is the value it provides high enough to justify its cost?
The answers to these questions will vary from project to project, for instance performance may be the most important aspect while on another it may be more important to have the web pages match UI designs perfectly. What is important to a project will also have a big impact on how valuable a test will be and it will be up to you to be pragmatic in finding which ones will be the most useful to you.
In the examples and explanations below, which are based on production code, we will be using Jest and react-testing-library, which are two of the most prevalent testing libraries currently in use in React, but the ideas should be transferable across technologies.
UI Testing
As the code for the UI / markup is usually quite simple, the tests for them should also be relatively simple and low cost. Having unit tests to verify that a specific component has been rendered, are easy and quick to write, and provide instant value. Choosing what query to use from React Testing Library is dependent on the specificity of the component and how often the page/component is going to change. If the query is moved into a variable, then it can be refactored easily in the future.
Starting with this and then refactoring later to a better querySelector may allow you to progress in your implementation and let it take shape before you know exactly how you want it to be.
Here are some tips on what queries can be useful where:
- If it only matters that the text is on the screen, searching by getText is sufficient
- Most queries will have an ‘All’ version (e.g. findAllByText) if you need to grab multiple elements
- Use queryBy if you are testing whether an element may or not be there, as findBy will throw an exception if it does not find a matching element
- Use findBy when dealing with asynchronous events
A full list of queries can be found here: https://testing-library.com/docs/queries/about
Testing styling
If a mature design system is used, for example a clear styling system using TailwindCSS, verifying that classes exist on an element may be valuable, but doesn’t need to be there from the start if perhaps you don’t have a concrete design yet, and could be refactored in later.
Having unit tests testing the specific order of components is not as valuable, as they can be brittle if your layout changes a lot, and may not even be verifying what you are trying to test accurately.
A test like this is trying to verify that the text in order of Div 1, Div 2, Div 3, but this could simply be changed in CSS, for example by using display: flex, and flex-direction: column-reverse, the order would be reversed but the test would still be passing.
Conversely, if you decided to add an element to the layout, for instance an element that returns the page to the top, it would not break the layout of the page, but would cause the test to fail.
Snapshot / Screenshot testing
These sorts of tests could be replaced with a snapshot test, which generates a snapshot of your layout, and will notify you of any changes. This is brittle but is a lot better than maintaining them yourself https://jestjs.io/docs/snapshot-testing
However, to actually make sure you don’t have visual regression, it might be worth using a screenshot test, as even if the markup is exactly the same, errant CSS changes could change the visual aspect dramatically https://github.com/fwouts/react-screenshot-test
Regardless of which methodology you use, testing styling can be very time consuming, both to maintain and to initialise, and will never be as good as manually testing and evaluating it yourself.
Logic Testing
With the UI and styling in place, the next would be testing the logic of the page or component. This would be well suited to be tested through having a test for each user story a page or feature may have. For instance if you have a login form, you would want to test every interaction a user may have with the page. For instance:
- The user will receive an error if they have not entered their email and hit the submit button
- The user will receive an error if they have not entered their password and hit the submit button
- They will login successfully when they have entered their email and password and hit the submit button
- The user will receive an error if their has been an issue with the endpoint when submitting their email and password
The tests should simulate each step that a user takes, and then verify that the correct response has been given.
Simulating user events
Events can be simulated by using the userEvent from react-testing-library, for example using userEvent.change to add a value into an input field, or userEvent.click to click on a button to submit a form.
Mocking functions
Be sure to mock any functions that may be called during your user flow that needs to be controlled or calls to an external source/API. There are several types of functions that could be needed to be mocked:
Mocking default export which is a function.
Mocking an export which is a function.
Mocking API Calls
Mocking an API call can be done by mocking your own abstraction, a HTTP client library like Axios or even the window itself. It could be best to mock your abstraction when testing at the component/page level, and then to mock the HTTP client library when writing tests for your abstraction.
You can then verify that you are calling your API with the correct parameters:
You will also want to mock when the API gives an error, which can be done with mockRejectedValue. For instance, if we have a signUp function which is called to make a call to our API:
This will allow you to wrap your API calls in a try / catch block, and ensure you are dealing with an error / status code correctly, for instance by checking if you display an error message on the screen.
Asynchronous events
When dealing with asynchronous events like API calls, be sure to wrap your assertions with a WaitFor, to ensure that your assertion doesn’t happen until asynchronous events have taken place.
Enhancements for your test suites
If you are running the same pieces of set up in each test a lot you can put them in a beforeEach inside your describe block and it will be run in each test. You can also make it more readable by extracting these repeated parts of code into functions as well.
You can also put mocks into your beforeEach if there is a common returned value, and override it in a specific mock in a test if needed.
If you are testing the same thing but with different values, it would be useful to parameterise your tests as well.
Conclusion
Just as you can break down your testing into different levels with TDD in the backend, you can also split where and how you test your application in the frontend and give differing degrees of importance to each section to fit your needs.
UI / Markup tests should be cheap and simple, verifying that the components/elements I expect are on the page and they contain the correct attributes or parameters. They are a good place to start when building tests for your pages/components.
Tests for styling are usually not very efficient or effective, but can be tested through either checking for classes if used with a good design system, or snapshot/screenshot testing to ensure visual regression has not happened. They may not be necessary in a lot of cases.
Logic tests should usually be where you spend most of your efforts in terms of testing your front end. They should verify behaviours and user stories, making sure that any expected user interactions are accounted for and are where you will find TDD provides a lot of effectiveness in your frontend application.