Originally published at coreycleary.me. This is a cross-post from my content blog. I publish new content every week or two, and you can sign up to my newsletter if you'd like to receive my articles directly to your inbox! I also regularly send cheatsheets, links to other great tutorials (by other people), and other freebies.
Have you ever tried Test-Driven Development (TDD) thinking it would be the "holy grail" it's often made out to be, only to end up feeling like it was pointless?
Maybe it didn't add any benefit to your code. Maybe writing your test first, then the code after felt uninspiring or limiting, or just the wrong way to do things, especially since the way programming is taught is code-first, not the other way around. Or maybe it just felt like a chore.
All the best developers seem to talk about TDD like it's the only way to code, and if you're not doing it that way, you're wrong. So you really want to like it. But if you've tried it and you didn't like it for any of the multitude of possible reasons, what is the point in practicing it at all? If only you could have that "aha moment" that made TDD make sense, you might actually enjoy it and feel like a "real developer."
I think the adoption of TDD is more often than not encouraged in the wrong way, and I'm going to show you how to think about it in a different way that will help you realize the benefits more quickly. Not because industry knowledge dictates it's what you're "supposed to do" or your team looks down on you if you don't develop that way, but because it can be one of the best tools in your toolbox to help you when you get stuck.
How it's usually encouraged
Much has been written about the value of TDD (writing your tests first, then writing the code). The usual benefits touted by TDD-adoption are:
- less bugs
- faster overall delivery
- smaller, single-responsibility functions
Less bugs, faster overall delivery, smaller functions - awesome. Some developers/teams really struggle with this, and for them the benefits will probably click more easily. But it still may not make sense to you why you should do it if you don't have many bugs or problems delivering code quickly and your functions are single-responsibility already.
And the "why you should do TDD" argument as above, while certainly developer-oriented (especially the last bullet), is more targeted at management. I've seen managers who haven't coded in forever, if at all, herald TDD as the "fix-all", suddenly mandating it as the new development-style, which ends up turning it into something that's been chosen for you, rather than something you've chosen. This doesn't help.
Thus, TDD can become something you feel you should do because:
- Your boss told you to
- The industry tells you to
- You're looked down upon by your peers if you don't
- You look down on yourself if you don't
Or maybe you don't have any of these pressures at all - you just don't get TDD. Maybe the long-term benefits of less bugs and easier to read/write functions is just too abstract right now.
But attempting to adopt TDD with this mindset, it can turn TDD into more of an adversary as opposed to something you do because it helps you - you as a developer who is under the gun to deliver on a feature.
Something more relatable
Instead of understanding TDD from a "best practices" perspective, I've found it easier to understand in more concrete terms, something more relatable.
As stated in the title of this post - to get TDD to "click" try using it the next time you are faced with writer's block, called "coder's block" from here on.
What is coder's block?
Have you ever been in a situation where you've been completely stuck trying to figure out how you're going to implement a particular piece of code? Maybe a deadline is approaching and you're really screwed if you don't get that code written, but you keep hitting coder's block and don't know how to start. Before I started using TDD to push through these block I used to just browse Reddit, HackerNews, etc. as a way of procrastinating. Either I was overwhelmed by a really hard problem and I didn't know where to start breaking it down, or it was just one of those days.
While "best practices" is abstract, I bet you've encountered coder's block lots of times. But you can use TDD here to help you out of that situation. Not because someone told you you're not a good developer if you don't, but because it helps you.
Side note: I'm not a TDD-purist. I understand it doesn't always make sense to write tests first (R&D work, initial proof-of-concepts/sketches, pure DOM/view code, etc.). But TDD as removing writer's/coder's block has been invaluable for me, which is why I recommend it here.
How to do TDD next time you get stuck
In order to demonstrate how you'd go about this, let's imagine a simplified scenario. You have a feature for an online shopping application you're working on in which the requirements are:
- Customer must be able to enter their preferences under a "Profile" tab
- Customer preferences must be saved
- Preference input fields must match some regex
Imagining you're stuck and aren't sure where to start, you could think about what the very first test you could write would be.
There are several requirements here, but you know you've got to manage the state of the selected/entered preferences, so that's a good place to start. The test, assuming the application is in JavaScript, might look like:
import {addPreferences} from '../preferences/preference.service'
import {Preferences} from '../preferences/preference.service'
let pref_service
describe('PreferenceService', () => {
beforeEach(() => {
pref_service = new Preferences()
})
it('should initialize state', () => {
expect(pref_service.preferences).to.deep.equal({
contact_method: null,
phone_number: null,
email: null,
preferred_shipping: null
})
})
})
This might not seem like much, but it's actually quite a lot. We've already figured out what shape our state/preferences need to be in, which is a meaningful part of the implementation. And more importantly, we began by not knowing where to start at all.
An example implementation of the code for that test might be:
export class Preferences {
constructor() {
this.preferences = {
contact_method: null,
phone_number: null,
email: null,
preferred_shipping: null
}
}
}
Cool, now another test:
it('should add preference to preference state', () => {
pref_service.setPreferences({phone_number: 'phone-number'});
expect(pref_service.preferences).to.deep.equal({
contact_method: 'phone-number',
phone_number: null,
email: null,
preferred_shipping: null
})
})
And that code:
setPreferences(preference) {
this.preferences = Object.assign(this.preferences, preference)
}
Start with one unit test, then the code. Another test, another piece of code. Now you're probably already over that block you had when you started.
Wrapping up
Thinking about TDD in this way will hopefully help you realize the power of it. A lot of getting TDD to "click" is getting into a rhythm. And more importantly, using it as tool to help you, not something that's a "best practice" you're following.
When you get going and get over that block it will start to make more sense. Just like how you break something down by writing a todo list, then you do the things on that list - using TDD to overcome coder's block and seemingly overwhelming features is the same mechanism.
This will ultimately be what makes you a better developer - overcoming blocks by learning to understand requirements and breaking down the problem into manageable parts. Not only will you spend more time coding - which itself will make you a better developer - but you'll know how to make things manageable.
The next time you're stuck try writing just one test before you write the code. Only one. Even by figuring out a starting point, this will greatly help in getting unstuck and give some direction, and even if you don't use test-first after those first few tests, you'll have figured out a path to implementation.
I think testing should be as easy as possible in order to remove the barriers to actually writing them. It's one thing to get stuck on code - you don't have any choice but to fix it. But its another thing to get stuck on tests - with tests you technically can skip them.
I'm trying to make testing and other things in JavaScript easier by sending out tutorials, cheatsheets, and links to other developers' great content. Here's that link again to sign up to my newsletter again if you found this post helpful!
Top comments (9)
One thing that made me "get" testing is that I'm going to manually test my application anyway — why not automate it? Life is too short to click through things / run functions to see their results. Once I'm writing automated tests, I realized that I'm going to inevitably forget to write them — why not write them first? Can't be too cautious, right? Thus I'm doing TDD ever since.
Totally, I had a similar realization when I first started getting into testing - "I know I need to write tests for this, so might as well write them first and get it out of the way"
But as I started to like writing tests more and more, "getting them out of the way" felt like less of a pain and some thing I enjoyed. For me that's when using TDD as a design tool really started to click.
Same. I can say as far that if I'm not doing TDD on a project, it's not a project that I take seriously. I hope I'm not growing into a TDD zealot, but it's a thing that makes too much sense to not do.
This was big for me as well!
Realizing that automated testing is just the more efficient testing workflow in most cases really helped me get into it. Then having the tests in the future is just an added benefit.
What sometimes still holds me back is tedious setup + having to mock things, creating extra work.
Still trying to get more efficient at that.
I definitely needed to read this. I tried out TDD for a project a while ago and was having trouble getting it to click. Reading this post and your examples makes much more sense to me than what I've seen elsewhere.
Awesome, glad to hear it was helpful. For me even just writing one test first a day - even if the rest were test after - helped it click even more and helped me turn it into a practice.
Really love the concept of using tests as a form of todo-list and as simply a way to think through/outline code in early phases.
As I mentioned in another comment, what sometimes holds me back is general setup required for testing, fake data, mocks, etc.
There's a lot of info on testing, but IMO not much on every day, practical testing practices that breaks down typical patterns and gotchas. Would definitely love more on that topic!
"using tests as a form of todo-list" -> this is a perfect way of describing it
And I definitely hear you on there not being much content out there on real-world unit testing. When I first started learning testing this was a huge barrier for me.
I've got a whole backlog of posts I'm planning to write that attempt to target exactly the pain you described - lack of practical testing examples. It's a pain I've seen so many people have that hopefully I can help make it easier and less painful. I've got a few I've already written posted here and the future ones I'll post on my blog and here on dev.to as well!
I'm exactly like this, but next time I will try writing a few unit tests to try to overcome coder's block.