Testing on the frontend
Testing your code, whether it's frontend or backend, is crucial for maintaining high-quality, accessible, and user-friendly applications. In this article, I'll focus on various testing practices that are particularly beneficial for frontend development.
Unit tests
Unit tests are designed to verify that individual units of code function as expected. For frontend development, this could involve testing a single React component or hook to ensure it behaves correctly in isolation.
Integration tests
Integration tests aim to evaluate the combined behavior of multiple units of code to ensure they work together as intended. This might include testing the interaction between a component and a hook within a page.
End-to-end tests (E2E)
End-to-end tests simulate complete user flows, from initial actions like signing up to accessing authenticated pages. Tools like Cypress and Playwright are commonly used for these types of tests.
Visual regression tests
Visual regression testing involves capturing snapshots of UI components or pages to detect any unintended changes or regressions. This is a key practice for maintaining consistent visual appearance across your application.
Accessibility
Testing for accessibility is essential to ensure that your application is usable by as many people as possible, including those with disabilities. Here are some approaches to accessibility testing:
-
Automated Testing Tools: Tools like Axe or Lighthouse can automatically scan your pages for common accessibility issues, providing a quick and scalable way to identify problems.
-
Manual Testing: While automated tools can catch many issues, manual testing sometimes may be needed to ensure comprehensive accessibility. This can include navigating your application using screen readers, keyboard-only navigation, and checking for sufficient color contrast.
-
User Testing: Involving users with disabilities in the testing process can provide invaluable insights into the usability of your application. This type of testing can uncover issues that automated tools and manual testing might miss directly from those who need accessibility.
Mocking
Mocking is a technique in testing that helps dealing with components or functions that interact with external services or APIs. You can simulate these external dependencies, allowing you to test the component in isolation and ensuring that your tests are not affected by the real behavior of those dependencies.
For example, when unit testing a component that performs data fetching from an API, you can use a mock to simulate the API response. This approach enables you to test how your component behaves under different conditions (like success, error, loading states...) without actually making any network requests. If you use Jest you can mock: functions, modules, and even entire libraries, making it straightforward to implement mocks in your tests.
But mocking is not limited to these situations. You can also use it to test components that rely on context providers, global state, or even browser-specific features like local storage. When you control the input and behavior of these dependencies through mocks, you create a more predictable and controlled testing environment, leading to more reliable and comprehensive test coverage.
Happy vs edge cases
It's easy to think about the happy path when testing. You can always assume the API will return the data that you need, right? That's why you also need to stress test your code with edge/border cases to make sure you are handling those situations properly on your code.
Things like wrong input and network errors will eventually happen if your application has enough users. You must take those extreme and sometimes rare cases into account when developing because not handling them will lead to a frustrating (sometimes annoying) user experience.
Running tests on push
Running tests locally before pushing changes is a good practice, but it's easy to overlook or forget, especially when working under tight deadlines or on complex features.
Incorporating automated tests into your CI/CD pipeline ensures that tests are run consistently on every commit and every merge to the main branch. This setup not only helps who is reviewing the code by highlighting potential issues early on, but also acts as a safety net, catching bugs before they can affect the end user's experience.
Besides that, integrating tests into your CI/CD process helps to foster a culture of quality and accountability within the team. It ensures that everyone contributes code that meets the project's standards, behaves as expected in isolation and doesn't introduce regressions. It can also save your team's time by automating it, allowing developers to focus more on feature development and code improvement.
By making it a part of your development workflow, you're taking a step towards maintaining a high-quality, robust application that delivers a predictable user experience.