You may remember how much did others tell you to write your tests first, see them fail, and only write your production code after that?
It's one of the craziest waste of time in software development, but not for the reasons you may think.
The #1 reason your teachers, mentors and your community forced you to write unit tests first was this: get testable code.
So, have you just accepted the implied message that if you don't write your tests first, your code will be untestable?
Does it feel like you always need to make an extra effort to turn your code testable but you're not exactly sure why?
Turns out, long story short, what makes code untestable is side effects. A function that changes global state, or the state of other objects, listens to global events, reads global state or the state of other objects and acts on them is not exactly an untestable function, but... almost. You need mocks, stubs, spies and reset them at every test run. Every single test may easily reach over 100 lines of code. It's a maintenance hell that must be fixed before a project grows large.
// an "untestable" function
function untestableOf(something) {
const rnd = Math.random();
const str = location.href +rnd;
location.hash = str;
if(document.cookie.indexOf('whack')) {
console.log('a mole');
window.googletag.que.push(() => {
document.addEventListener('click', () => {
googletag.pubads.refresh();
})
})
}
return rnd > 0.5 ? something.x : something.y;
}
// shall we go on?
Side effects are just something you learn to do at school, from day one, when writing your first Hello World application. An unfortunate and involuntary disinformation campaign led by most schools who teach programming.
Had they taught you to use pure functions from day one, instead, you'd not have heard of TDD, most probably.
OK, it's broken. Can we fix it?
What's all that jazz about pure functions?
Well, it's easy: their return value only depends on their parameters, so no side effects, and... no mocks, no stubs, no spies are needed. A first big step to cut 100-liner unit tests by ~90%.
function testable(a, b, c) {
return a+b/c;
}
Testing the above function only needs you to cover all paths and edge cases that could lead to invalid or undesirable output.
Down with side effects? Not really
Side effects are not an evil to get rid of. Without them, in fact, you couldn't do literally anything, not even printing Hello World to the screen.
The goal, instead, is to isolate them, put them in order. What that means is if youlook at you application's AST, effects should be performed at the bottom by utilities (e.g.: getCookie, postMessage, etc).
What's left is by necessity, either pure functions or intermediary functions that call them.
Pure functions are testable by default 🚀
Ok, so here we came to a turning point. If pure functions don't need mocks, stubs or fakes, that means they immediately become "testable by default".
Looks like we just removed the main reason for TDD, which is forcing ourselves to write testable code. That's a great milestone, but let's celebrate quickly 🥳🎉🎆 so we can move on.
What's DDT?
DDT, a quickly made-up acronym for "Development-Driven Testing", actually known as "testing after the fact", is a derogatory expression with the meaning of not doing proper QA.
However, as we just removed the main issue TDD was trying to fix with our testable-by-default code, we should feel no shame for testing after the fact. We can model our tests conveniently after refactoring and finessing our production code, so long live DDT!
No more trashed tests
The most horrific disadvantage of TDD is seeing tests thrown away and rewritten evry time we refactor. It's really hard for some people to mentally map the code in their mind with their exact behaviour and start with tests first.
It always ends up with code being refactored many times over (so most tests will need rewriting, too), or a tendency to oversimplify functionality just to make test trashing less expensive.
Conclusion
TDD’s emphasis on writing tests first stems from a world where untestable code was the norm. But by prioritizing pure functions and isolating side effects, you can create code that’s testable by default. This frees you from TDD’s rigid workflow, reduces test maintenance, and eliminates the fear of "untestable" code. So, ditch the dogma, embrace Development-Driven Testing and write better software with less wasted effort.
Top comments (0)