What is Test Driven Development?
JavaScriptTest Driven Development (TDD)
Test-driven development (aka TDD) is a three-step process. It's often referred to as the "red, green, refactor cycle"
Here's the way it works:
- 🚨 Red: Write a test for the function/module you're going to create before it exists/supports the feature you're adding. This gives you a test that fails (you get a "red" error message).
- ✅ Green: Implement just enough code to get that test passing (you get a "green" success message).
- 🌀 Refactor: Look over the code you have written and refactor it to ensure it's well-written, as easy as possible to read/understand, and well-designed. (The cool thing with this step is that you now have a test in place that will tell you if you break something as you refactor.)
- 🔁 Repeat: It's a cycle, after all. Keep going until you've finished implementing everything you need to.
When does TDD make sense?
It's really an intuition you develop and has a lot to do with your comfort and experience with TDD, but here are a few examples of situations where I follow the red-green-refactor cycle of TDD.
Fixing a bug
When fixing a bug, it's valuable to reproduce that bug with a test before fixing it. This approach provides confidence in understanding the root cause of the bug, and when the test passes, it confirms that the bug has been properly fixed rather than just working around the problem.
This approach is particularly valuable in production software and open source libraries, where maintaining reliability is crucial.
Pure functions
Pure utility functions don't always need isolated tests (they can be covered by integration tests), but for complex utility functions that warrant unit tests, TDD is an excellent approach. These functions typically have well-defined sets of inputs and outputs based on specific requirements.
A great example of this is currency formatting logic. Consider a payment processing system that needs to format amount inputs as users type. This can be surprisingly complex due to currency precision rules - some currencies don't support decimal amounts, while others might have different decimal place requirements. Such formatting logic is perfect for TDD because the possible inputs and required outputs can be clearly defined upfront.
Well defined user interfaces
Testing implementation details can be counterproductive and slow down development. The core purpose of TDD is to help developers think about what they're building from the outside in, focusing on behavior rather than implementation. This approach is particularly valuable when you know what you need to build but haven't yet determined how to build it.
Earlier testing tools often encouraged testing implementation details. This meant developers needed to know implementation specifics beforehand (such as creating a private method called makeDonation that would be called with specific parameters like amount and currency). This approach to TDD often felt mechanical and less valuable.
Modern tools like Testing Library have transformed this landscape by emphasizing user experience over implementation details. This shift makes TDD much more practical for UI development, especially when working with components that have well-defined designs and user experiences.
For example, when building a login component, tests can focus on user interactions like entering credentials and submitting the form, rather than testing specific method implementations. This approach better reflects how users actually interact with the application and leads to more maintainable tests.