Test-Driven Development (TDD) for Frontend Engineers: An In-depth Guide
Test-Driven Development - also known as TDD - is a methodology to write (failing) tests before your write the code for the feature you're developing.
At its core, this is all TDD involves
- Write a test that fails, for the smallest feature possible (e.g. 'when button is clicked, a counter appears')
- Write the production code so the test passes. Don't worry about code quality.
- Now you have passing tests, refactor the code to be nicer quality.
- Then repeat, with the next smallest feature
I remember trying it years ago, and deciding that it resulted in messy code and horrible tests. I think I just missed the core part of TDD.
TDD isn't about getting test coverage.
TDD is about writing your code in a way that makes it easy to test. And if it is easy to test, it is much more maintainable.
Of course, it does help that as you build features with TDD they almost always have good code coverage. You also tend to come up with more edge cases to account for, as you're writing the failing tests first.methodology
In a non TDD workflow (write code, then write tests) it is much easier to fall into pattern of mostly testing the happy path. But what if you click twice? What if you use a keyboard to trigger the on-click
on the button? etc
Quick note about TDD & Frontend applications
In this blog post I'm going to give examples of tools to use with React. But TDD in frontend applies to any frontend framework - such as VueJS, Angular, Svelte, etc. I'm picking mostly on React as it is by far the most popular.
Red/Green/Refactor
As mentioned, the core part of TDD is to write a failing test, write the smallest amount of code so it passes, then refactor.
This is known as red/green/refactor.
Setting up your local dev environment for best DX for TDD
You really want to aim for fast running unit/integration tests (every company has their own definition of what these mean).
Ideally with a reliable test runner. I always use jest
. I know some love mocha
but it seems quite rare now in 2023 to see new projects use mocha
.
E2E (end-to-end) tests which use Cypress or Playwright can be useful, but they tend to be quite slow. The slower your feedback while doing TDD, the worse experience you will have writing TDD code.
Unit vs Integration vs E2E
Unit tests (frontend)
Unit tests are testing a single 'unit' of code. In a frontend application this almost always refers to
- a single JSX React component
- or a single function (running some business logic)
For component tests in a React application
Integration tests (frontend)
Integration tests are testing that several 'units' of code work together. In a frontend application this normally means a larger component built up of several other components. An example would be:
- a multi step wizard checkout flow
- a NextJS 'page', which loads different components within it
E2E (End to end) tests
E2E tend to test the entire application, as a real user.
They will tend to use tools such as Cypress or Playwright. They run a real version of a browser (often Chrome, and often 'headless') and interact like a real user.
They are slow, harder to maintain. But you get a lot of confidence that your application is really working like it should. Even more so if you do real API calls.
Best practices when doing TDD
Treat your test code (after refactoring/cleaning up) to the same level of quality as production code.
I think you can sometimes get away with more duplication (makes tests easier to read), but generally test code should be as good quality as your main application code.
But make good use of utility functions and fixtures for tests
Create helper functions to generate mock data, or do common actions. For example if you have a component that pops up a modal, you probably want to interact with elements inside the modal. Instead of writing the code to trigger the modal, it could be useful to have a helper function to trigger this common action.