Automated Testing

TDD: The Art of Failing First

Author

Hector C. Ortiz

Date Published

image of test suite results

TL;DR

I rediscovered an old project I built while learning TDD from a book, and each commit shows a tiny, isolated step of the TDD cycle. The post gives a quick breakdown of how functional tests capture the user’s journey end-to-end, while unit tests focus on the individual pieces of logic under the hood, together forming the backbone of Test-Driven Development.



Note: This post is based on the book Test-Driven Development with Python.

Heres a link to the repo incase you are interested.



While scrolling through my GitHub repositories the other day, I stumbled across a project I built a while back. I created it specifically to put TDD techniques into practice while I was learning them, and later used it for a presentation to the engineering team at Red Ventures. The cool thing is that, each commit is a tiny, isolated step in the TDD cycle, like digital breadcrumbs showing how the app evolved from absolutely nothing to “okay, this actually works.” I figured it would be useful to distill that experience into a quick blog post.


Functional Tests: The User’s Journey

First, let’s talk about functional tests (a.k.a. acceptance tests, end-to-end tests, or the tests that make you question your life choices when they fail). These tests tell a story, the journey of a user moving through your application. If you sit down with your product manager and have them walk you through a typical user session, from landing on the page to achieving a goal, that’s essentially what a functional test captures.


Functional tests make sure the entire flow works smoothly from start to finish. It’s also helpful to use comments to outline the user’s journey and assert each step along the way.


Screenshot of functional tests


Unit Tests: The Programmer’s View

Now let’s talk unit tests. While functional tests look at the app from the outside (the user’s perspective), unit tests examine it from the inside (the programmer’s perspective). They ensure each individual function, method, or class behaves exactly as intended.


If functional tests confirm the entire experience works, unit tests confirm the logic behind the scenes is solid. They’re your safety net as you build and refactor.


Sample of a unit test


Red, Green, Blue

So how do we put this into practice?


  1. Red: Write a functional test that describes what the user wants. It fails. Immediately. Spectacularly. This is fine, it's supposed to happen (you’re building character). Next, think through the code needed to satisfy that behavior and write one or more unit tests that define how the logic should work. Those will fail too, and that’s still exactly where you want to be.


  1. Green: Then write just enough application code to make the unit tests pass. Once they pass, rerun your functional test and get it passing too.


  1. Blue: Congratulations! You are now allowed to refactor. This is where you clean up the code, reorganize things, and make everything as elegant as you want. Thanks to your tests, you can refactor with confidence instead of fear-sweating.


In essence, functional tests drive development from a high level by focusing on the user experience, while unit tests guide development at a low level by ensuring each piece of logic is correct. In a follow-up post, I’ll be exploring how AI can fit into this workflow and how TDD practices evolve when paired with modern AI tools.


Final Thoughts

TDD is a discipline. It doesn’t always feel natural at first, mostly because humans like instant gratification, and TDD is the broccoli of development, healthy, long-term payoff, minimal dopamine.