There is fear in our community that using Test Driven Development (TDD) leads to a painful developer experience (DX). That TDD is not fun or easier than the alternatives. However, I will attempt to present the case for how TDD improves the developer experience. This case will be both subjective and philosophical, so buckle up.
My most enjoyable coding experiences are when I am in a state of flow. We can enter "the zone" or a flow state by
- Having a clear set of goals, direction, and structure for the task.
- Having clear and immediate feedback on progress with the task.
- Having an optimal balance of perceived task difficulty vs perceived confidence and skill to complete the task.
- Having few distractions while completing the task.
Test Driven Development helps us enter a state of flow while coding, which makes coding enjoyable.
This is my experience with Test Driven Development:
- I enjoy consistent and immediate positive feedback from the test runner while developing features. The structure of TDD enhances my chances of entering a flow state.
- I enjoy lots of small wins throughout the day as I write tests, pass tests, and clean the code.
- I enjoy compliments from teammates when we do code reviews and they understand what my code is supposed to do.
- I enjoy uninterrupted coding because of the executable documentation provided to my coworkers by solid tests.
- I enjoy the confidence I have that my modules will work as expected.
- I enjoy confidence in my ability to make small working code units. I enjoy confidence regarding my own skill to compose together small well encapsulated units using integration tests. I enjoy confidence that many small units working together can eventually solve large problems.
- I enjoy the lack of distractions when working on one test for one unit.
- I enjoy fast unit tests with less context switching, rather than end to end or manual tests.
- I enjoy waking up in the morning, and having a clear idea of what needs to be done in the codebase because I have a failing test to start from.
- Most of all, I enjoy seeing my code working and providing value to people in production.
TDD helps make coding fun by leading to a positive DX.
- I suffer when I get stuck because a unit is broken, but I don't know which one.
- I suffer when I force my brain to evaluate code and/or keep track of output in meat space when I could leverage the computer to do it for me.
- I suffer when I worry about multiple levels of abstraction at once, rather then just the system under test.
- I suffer when I write low quality code I'm not proud of in order to hit an arbitrary deadline.
- I suffer when I force end users to find and report bugs. I suffer from the context switch related to stopping other tasks to handle the bug. I suffer while reading the bug report, which is often missing important details needed to fix the bug. Forcing users to find our bugs leads to upset users, a bad reputation for the product, and damaged egos for the development team responsible.
- I suffer when I have to wake up in the middle of the night from preventable alerts and outages related to edge cases and bugs.
But most of all:
- I hate being scared of changing my own code because I worry about causing regressions and bugs. I hate being scared of working on something I created.
TDD practiced well is like a video game 🕹️ where you can see your score and XP only rising as you slay bugs and create features. I feel satisfied when I finish a day with more passing than failing tests. I hate video games where you can't track your progress or your statistics. I also hate "grinding" out manual test runs when I know how to automate them with a bot.
TDD should be pleasurable, not painful.
I find that when TDD is painful, this is crucial feedback about my design.
I am the first to admit that some parts of TDD aren't "fun". But there are ways to minimize the trade offs.
TDD is slower than the alternatives. Taking the time to design clean modules, to write tests first, and to constantly refactor the code takes time and patience. However, I subscribe to the philosophy that
“Slow is smooth. Smooth is fast.”
Smooth progress is also much less stressful than coding yourself into a corner.
TDD sometimes involves mocking to isolate the systems under test. I don't enjoy mocking. I find complex mocks hard to maintain. Mocks can also drift away from the real thing, and have a cost to maintain. But most importantly, mocks don't directly help end users. I treat excessive mocking as a code smell.
We need to mock when unit's have external dependencies or side effects.
We need to use spies when we can't test the output of a unit.
To solve these issues, we can refactor our code to use pure functions.
We can also use boundary classes to wrap external dependencies, which can lead to fewer mocks being needed for our business logic.
I have also found reducing a unit's responsibilities lead to less mocking per unit. Additionally, shrinking a unit usually leads to it having a single responsibility, which is excellent for maintainability. Smaller units may also be easier to reuse, leading to DRYer code.
I also recommend refactoring code to be loosely coupled. Loosely coupled code can be tested on its API level with fewer mocks and spies.
Good design can lead to a better DX. The Red-Green-Refactor cycle of TDD can help us change our design iteratively, moving incrementally closer and closer to a clean design and more enjoyable TDD.
This post is already a bit long, so I won't offer solutions for the following just yet. If you're interested in my solutions to the following, leave a comment below!
- There are many projects with technical debt. Paying off technical debt is always painful, regardless of the use of TDD.
- It's hard to unittest code with cruft (leftover code from previous iterations). It's hard to know which modules can be removed and ignored, and which need to be updated. We should constantly clean our code, the same way we constantly clean our physical surroundings.
- It's hard to unittest code without dependency injection.
- It's hard to unittest when you have a demanding schedule in the short term.
- It's hard to convince team members to change their practices.
- It's hard to give constructive criticism to team members, and to talk about better ways to work without blame.
- It's hard to convince management that the time invested in TDD isn't wasted. The time savings for TDD mainly pay off in the long term while the cost is in the short term.
- It's hard to test spagetti code, where pulling on one strand causes unintended consequences in it's tightly coupled sibling.
I hope this post gave you some food for thought.
What do you enjoy most about TDD? What part of TDD is the hardest for you?
Please leave me your thoughts and comments below.