DEV Community

Mateus Cechetto
Mateus Cechetto

Posted on

When does TDD make sense?

Throughout my career, I often heard that Test-Driven Development (TDD) is an effective approach for building software. However, I struggled to see the benefits for a long time. This changed recently when I was working on a project where TDD was an ideal fit. In that instance, it significantly improved my development process, making it faster and less prone to errors. In this article, I will explain when to use TDD and why it works best in certain scenarios.

When TDD Falls Short

While TDD is a powerful methodology, it's not always the right tool for the job. Here are a couple of scenarios where applying TDD can be more problematic than beneficial:

  • Unclear or Evolving Requirements: When requirements are ambiguous or still evolving, writing tests upfront can feel like shooting in the dark. In these cases, we need to explore the problem space, experiment with different approaches, and iterate on the design as we learn more. Trying to lock in tests before understanding what the code should do wastes time and stifles creativity.

  • Low Domain Logic: In cases where the codebase is mostly concerned with handling input/output (I/O) operations or simple tasks, there’s little value in writing tests first. For example, building a "Hello World" program is a clear case where TDD is overkill. The code is too simple, too stable, and has little to no logic to test. If the code heavily interacts with external systems like databases, file systems, or APIs, writing unit tests upfront can be inconvenient and less effective. The high ratio of boundary code (e.g., I/O) versus actual domain logic means that the return on investment for TDD is low.

When to use TDD

Now let’s look at when TDD shines. From my recent experience, I found that TDD works best in scenarios where:

  • Clear Requirements: In the project I mentioned, the requirements were crystal clear. I knew exactly what the expected output of each function should be for every type of input. This allowed me to write the tests first with confidence that they reflected the desired behavior. Because of the clarity, the tests guided the development, ensuring that my implementation was correct and met the business needs from the get-go.

  • Complex Domain Logic: If you’re working on a system with a lot of complex business rules, TDD becomes incredibly valuable. In this case, each test verifies that a specific part of the logic behaves correctly. I experienced this firsthand: after adding each new piece of functionality, I ran my tests to see what was missing and iterated on the code until all tests passed. This made me confident that each new change didn’t break any existing behavior.

Why to use TDD

  • Behavior-Driven Focus: Writing tests first helped me focus on the behavior, not the implementation. This is an essential distinction, as it prevents tests from being too tightly coupled to the internal workings of the code. Instead, the tests reflect what the code should do, making refactoring easier without breaking the tests unnecessarily.

  • Long-Term Maintenance: One of the biggest advantages of TDD is the long-term stability it brings. As I revisited the project later to add new features or improve existing functionality, my existing tests acted as a safety net. I could confidently make changes without fearing regressions because the tests ensured everything still worked as intended.

Pratical Example: Building a Custom Deck Validation DSL with TDD

In one of my recent projects, I applied TDD to build a custom Hearthstone deck validation Domain Specific Language (DSL). The goal was to create a system that allows users to define complex deck-building rules in a human-readable format. First, I designed how the language would look like, and what scenarios it should cover. This clarity in requirements, coupled with the system's complex logic, made it an ideal use case for TDD.

The project had two core components that benefited greatly from a TDD approach:

  • RuleValidator: This component is responsible for validating a user’s input to ensure it follows the DSL’s syntax and semantics. It tokenizes the input, checks for errors in structure, and returns a list of validation errors with clear messages for the user. If the list is empty, it means the input is valid. The TDD approach ensured that all possible validation scenarios, including edge cases, were tested during implementation.

  • RuleGenerator: Once an input is validated, the RuleGenerator transforms it into TypeScript code that defines the deck-building rules. It first invokes the RuleValidator to confirm that the input is correct. For valid input, it generates a function representing the rule, based on attributes, operators, modifiers, and values. This generated code is then used by the DeckValidator to verify whether the cards in the deck follow the defined rules.

By writing tests first, I ensured that all scenarios designed for the DSL were covered, which guided the development process from start to finish. The tests acted as a checklist, helping me verify that each feature was implemented correctly and completely. This process also made development smoother—rather than relying on memory to track what still needed to be done, I simply ran the test suite and worked through any failing tests.

The benefits of TDD became even more apparent when I refactored the code. For instance, I broke down larger functions into smaller, reusable ones, improving the overall design. Additionally, when I decided to add new modifiers (> and < for comparing values), I was confident that the existing functionality would remain intact because the tests served as a safety net, catching potential regressions.

Conclusion

TDD is a valuable methodology when used in the right context. It excels when you have clear requirements, a high level of domain logic, and need a reliable way to prevent regressions over time. However, it can slow you down in exploratory phases or for trivial, boundary-heavy code. Knowing when to apply TDD and when to hold off is the key to getting the most out of it.

Top comments (0)