ptb.dev

Comprehensive Testing for React

Testing is a core practice of professional software development. In this article, I'm going to discuss automated testing in React web apps specifically; that is, tests which exercise the desired functionality, are committed beside the application code, and are automatically run as part of a continuous integration (CI) process before merging.

Why Test?

  • Improved Code Quality: Automated tests can help detect bugs early in the development cycle, reducing the likelihood of defects in production. Tests provide a safety net when refactoring code, ensuring that changes do not introduce new bugs.
  • Faster Development Cycle: Identifying issues early in the development process can save time and resources compared to fixing bugs discovered later. Automated tests enable continuous integration and continuous deployment (CI/CD), speeding up the development and release cycles.
  • Documentation: Tests serve as a form of living documentation for the codebase, illustrating how different parts of the application are expected to behave. Writing tests often requires clearly specifying the expected behavior of the code, which helps clarify requirements and design decisions.
  • Increased Confidence: A comprehensive test suite provides confidence that the application works as intended, even after changes or additions. Tests help ensure that new code does not break existing functionality, reducing the risk of regressions
  • Maintainability: Well-tested code is easier to refactor, as tests can quickly reveal if changes have unintended consequences. When a test fails, it provides a clear indication of what part of the code is malfunctioning, simplifying debugging.
  • Cost Efficiency: Automated tests reduce debugging time and QA, leading to lower development costs. Preventing bugs from reaching production lowers maintenance costs and effort associated with fixing issues post-release.
  • Enhanced Collaboration: Tests help communicate how the system should behave to all team members, fostering better collaboration and understanding. Tests are part of each pull request's code review of the application code to ensure that the code is functioning as intended.
  • User Satisfaction: Regularly tested code is more likely to be stable and reliable, leading to higher user satisfaction. Users can trust that features work as advertised, increasing their confidence in the product.
  • Scalable Testing: Tests can be automatically run on different environments and configurations, ensuring that the application behaves as expected on multiple platforms.

What is Unit Testing?

Unit testing involves testing functions in isolation to ensure they work correctly. Ideally, unit tests are small and fast, focusing on specific pieces of code.

Stop Using console.log?

If you find yourself running a piece of code and using console.log to see the output, it's time to put that code into a unit test. While console.log can be helpful for quick debugging, it doesn't provide the repeatability and reliability that unit tests offer. Unit tests not only confirm that your code works correctly now, but they also ensure it continues to work correctly in the future as your codebase evolves.

If it's worth checking with console.log, it's worth validating with a unit test.

Example unit test

What is Component Testing?

Component testing involves testing individual React components to ensure they render correctly and behave as expected. Until recently, React component testing primarily relied on React Testing Library and JSDOM. While these tools are efficient and performant, they simulate a browser environment rather than using a real one. A better alternative is Playwright: a modern testing framework that supports real browsers, including Chrome, Firefox, and Safari.

  • Real Browser Environment: Testing in real browsers is more accurate, catching bugs that might be missed with JSDOM. Playwright allows you to run tests across different browsers, ensuring your application works consistently for all users.
  • Enhanced Debugging: Access to browser developer tools during tests makes debugging easier and more efficient. Playwright can take screenshots and videos of test runs, providing valuable insights into test failures.
  • Advanced Features: Playwright handles complex user interactions with ease, allowing you to test more realistic user scenarios. Intercept network requests and API responses to test how your application behaves under different conditions.
Example component test

What is End-to-End (E2E) Testing?

End-to-end testing involves testing complete application workflows from start to finish, simulating real user scenarios.

End-to-end tests ensure that all integrated components work together as expected. These tests validate interactions between different systems, such as authentication, APIs, and third-party services, ensuring seamless integration.

E2E tests help identify issues that may arise when different parts of the system interact, which unit or component tests are not designed to catch. These tests can reduce the need for repetitive manual testing, freeing up manual testers to focus on higher-value tasks.

Example end-to-end test

What is Code Coverage?

Code coverage is a metric used in software testing to measure the extent to which the source code of a program is executed when a particular test suite runs. It provides insights into which parts of the codebase are being tested and which are not, helping to identify untested or under-tested areas.

Code coverage is a valuable metric in software testing, providing insights into how much of the codebase is exercised by tests. While it is an important aspect of testing, it should be used in conjunction with other testing practices to ensure comprehensive and effective test coverage. Achieving high code coverage can lead to improved code quality and reliability, but it is crucial to focus on the quality of tests rather than just the coverage percentage.

  • Code coverage helps identify parts of the code that are not covered by tests.
  • Higher code coverage can increase confidence when refactoring, as tests can catch any regressions introduced during the process.
  • Code coverage helps assess the effectiveness of the test suite, identifying inadequately tested parts of an application.
  • High code coverage does not guarantee the absence of bugs! Tests may not cover all edge cases or potential error conditions.
  • Aiming for 100% test coverage is often unnecessary. Focus on high quality useful tests on critical parts of the codebase. Writing superficial tests to increase coverage can lead to a false sense of security.
  • Ideally, code coverage should measure all types of testing: unit, component, and end-to-end tests. However, it is not necessary to test the same section of code with all three methods, as this can be redundant.