What is Test-Driven Development?
Test-Driven Development (TDD) is a methodology in software development that focuses on an iterative development cycle where the emphasis is placed on writing test cases before the actual feature or function is written. TDD utilizes repetition of short development cycles. It combines building and testing. This process not only helps ensure correctness of the code -- but also helps to indirectly evolve the design and architecture of the project at hand.
TDD usually follows the "Red-Green-Refactor" cycle:
- Add a test to the test suite
- (Red) Run all the tests to ensure the new test fails
- (Green) Write just enough code to get that single test to pass
- Run all tests
- (Refactor) Improve the initial code while keeping the tests green
- Repeat
This process sounds slow, and it often can be in the short-term, but it does improve the quality of the software project in the long-run. Having adequate test coverage acts as a safeguard so you don't accidentally change the functionality. It's much better to catch a bug locally from your test suite than by a customer in production.
Finally, test suites can encapsulate the expectations of your software project so that your project's stakeholders (peers, future self) can better understand the project.
Benefits
TDD encourages writing testable, loosely-coupled code that tends to be more modular. Since well-structured, modular code is easier to write, debug, understand, maintain, and reuse, TDD helps:
- Reduce costs
- Make refactoring and rewriting easier and faster ("make it work" with red and green stages, then refactor "to make it right")
- Streamline project onboarding
- Prevent bugs and coupling
- Improve overall team collaboration
- Increase confidence that the code works as expected
- Improve code patterns
- Eliminate fear of change
TDD also encourages constant reflection and improvement. This often exposes areas and abstractions in your code that need to be rethought, which helps drive and improve the overall design.
Finally, by having an extensive test suite in place that covers nearly all possible paths, developers can get quick, real-time feedback during development. This reduces overall stress, improves efficiency, and increases productivity.
Iterative Process
If you're new to Test-Driven Development, remember that testing is an iterative process. Much like writing code in general, when writing tests try not to get too stressed about writing the perfect test the first time around. Write a quick test with the information that you currently have and refactor it later when you know more.
Approaches
There are two main approaches to TDD -- Inside Out and Outside In.
Inside Out
With the Inside Out (or the Detroit School of TDD or Classicist) approach, the focus is on the results (or state). Testing begins at the smallest unit level and the architecture emerges organically. This approach is generally easier to learn for beginners, attempts to minimize mocking, and helps prevent over-engineering. Design happens at the refactor stage, which can unfortunately result in large refactorings.
Outside In
The Outside In (or the London School of TDD or Mockist) approach focuses on user behavior. Testing begins at the outer-most level and the details emerge as you work your way in. This approach relies heavily on mocking and stubbing external dependencies. It's generally harder to learn, but it helps ensure that the code aligns to the overall business needs. Design happens at the red stage.
Which approach is better?
Neither. Try each of them. Use them when appropriate.
It's often easier to use the Outside In approach when working with complex applications that have a large number of rapidly changing external dependencies (i.e., microservices). Smaller, monolithic applications are often better suited for the Inside Out approach.
The Outside In approach also tends to work better with front-end applications since the code is so close to the end-user. See Modern Front-End Testing with Cypress for details.
Frequently Asked Questions
Why is it important to see a test fail?
It may seem odd, but it's just as important to see a test fail as it is to see a test pass in TDD.
Put simply, a failing test:
- Validates that the new test is meaningful and unique, helping to ensure that the implemented code is not only useful but necessary as well.
- Provides an end goal, something for you to aim for. This focuses your thinking so that you write just enough code to meet that goal.
What to learn and practice Test-Driven Development? Check out the TestDriven courses.