When you read about or hear about Test Driven Development, the first thing folks are always told is that in a TDD cycle, the first step is Red. You want a test that after it can compile, fails for the right reason.
What you are never told is HOW to get to Red. The #1 question I get from newbies trying to learn TDD, and even seasoned practitioners can struggle with this, is what test should I write?
Therein lies the first step in TDD. It's not red, it's Think! This is how to get there. It requires us to slow down and to stop rushing.
So...how do we think?
TDD is about the Journey
Before we talk about how, remember that Test Driven Development is really a journey guided by test driven design. It's evolving your design in order to provide some small piece of value to your end user.
Every TDD cycle moves us closer to completing the journey. Moving deliberately and safely as we travel through thoughtfully the codebase at a sustainable pace.
Meet Your Guide: A Combination of Dialog, Describes & Tests
We have stuff to deliver, we don't want to have to spend all day to read the code.
As a developer first and foremost you are your own customer. Collective Code Ownership requires that we can all read the code and lay of the land quickly and easily.
You the developers are the ones reading all the tests and the production code over and over again day in and day out. We spend more time reading the code than writing it.
Therefore writing well-named tests and production code is the one of the key attributes of a codebase that helps to enable true Agility and allows you to pivot quickly as requirements change. And we take this for granted far too often.
In order to write tests that document what you're about to TDD, you must have conversations with your team and the business and each other early and often. You must learn the customer's business domain which is part of THINK, the first step in TDD.
Then, writing a combination of test describes and underlying tests, you eventually finish that journey and push that small piece of value to production.
Don't Jump the Gun
When we work in a TDD workflow, we often feel like jumping the gun, starting inside the test trying to implement details of the test. We prematurely start thinking about how the public contract should change in the code under test.
What we should do instead is to stop, back up, and THINK first a bit about what we're trying to do next. We do this initial thinking through our test name.
By doing so, we can:
uncover misconceptions about requirements and the why behind the value we intend to test drive
- discover a smaller, safer step we might take in order to get faster feedback
- uncover more domain terminology up front
- prevent yourself from getting into rabbit holes throughout the journey
- help guild the wishful thinking portion (implementation) of our test
Creating Test Names Guides Your Overall Journey
Writing tests with pure behavior can and should be done at any layer in your application. Even down to the lowest level unit test.
Once we have a test name, then it can guide our test's implementation using wishful thinking. Our wishful thinking always reverts back to our test name. We can then come up with a more straight forward path to implementing the behavior we desire.
Good test names are written via well written prose. They are ignorant of the how. They contain domain terms, not implementation details.
But how can we come up with these?
Coming up with a Test Name
Here are a few steps you can use to come up with that next test name:
√ Do you have a small scenario you're trying to test drive?
If not, come up with one by having conversations with the business, your pair, testers, designers, PMs, or whoever. This is the first step. If you do not have this, then literally what are you doing next and why? This should be a very bad itch for you that you should take care of now.
√ Are you stuck for words in your test name?
Write out some domain terms that might make sense. Don't know them? Good, this is a hint that it's time to discover a little more. Take a few minutes and THINK. Talk to your pair, pull in a business stakeholder. We're not asking for an all day session, just a few minutes.
√ Look at the last test you just wrote
Think about what the smallest step next will be to inch you closer to completing that scenario.
√ Perhaps your previous test was too big of a step?
The previous test was too big to begin with, and that is a code smell. You are left with no ideas because that previous test did / covered so much behavior already and you probably realize now that you should have taken smaller steps by writing smaller tests next time.
√ Try creating a CRC card
Creating a CRC card with your pair (or just you) is diving into implementation (wishful thinking) a bit first, but if you're really stuck, it might help you think about domain terms. Once you have a CRC card created, then use those terms in it to help create your test name, before implementing the test. You do not need to be in a class oriented language to utilize these.
😭 Ah! I give up, I am simply lost!
If you are totally lost, tired, etc. then go back to your roots. Look at the scenario. Look at the describe under which you are writing tests for. The describe should describe that scenario. Again, look at the previous test you just wrote. That might trigger thoughts again as to where you might want to go next.
🥳🍻 Perhaps there are no more tests to write
You literally cannot think of another test, and you realize that the scenario you are test driving for is working! It is done! Done enough! Don't over think it. Push this to production, lets get feedback.
What Good Test & Describe Names Look Like
It's not enough to talk about test names, lets see some examples of real-world test names that describe behavior and domain, not implementation details.
describe("Add Application Confirmation"...
sets isStoringToAWS to true when saveToAWS call is in progress
sets storing status to in progress when saving a project
sets aws status to in progress when saving to aws
sets status to in progress when saving company's list of locations
shows error modal when saving application fails
presents an error call to action when saving an application fails
should render fallback banner if that field is undefined
shows a fallback banner when none exists
should display an Add a Requirement button
displays an add a requirement option
describe("when currentStep is the last step"...
it("should display an Add a Requirement button"...
describe("When current step is the last step"...
it("displays an add a requirement option"...
should display Launch button as disabled when disableNext is true
shows a disabled launch option
displays introductory text on the page
displays introductory text
renders no cards when there are no projects
shows no projects when there are none
should display 404 when there no application is found
displays an error page with copy
should display an input field for description
displays a field for specifying a description
numberOfApplications renders kinds of applications on the page
indicates how many of each kind of application there are
calls the graphql endpoint and maps the program data from AWS
retrieves a list of programs
when submitting a form
when submitting an application
should call setForm data with user input when form is submitted
sets user data when submitted
Notice these test names above do not contain:
🚫 delivery mechanism (form, page, modal, button, http, api, etc.)
🚫 function names
🚫 DOM tags
🚫 library or tech terms
They instead describe pure behavior independent of how.
Here is a list of various implementation details we removed: "form", "AWS", "isSavingToAWS", "saveToAWS", "modal", "button", "page", "null", "input" (when referring to type of DOM tag), "numberOfApplications", "ViewCompanyPage", "cards" (from a UI framework), "disableNext", "404", "setForm"
Domain terms used: "program", "company", "application", "application confirmation"
By writing test names like this first it:
1️⃣ Helps guide you in thinking about domain language as you implement the test with wishful thinking
2️⃣ Decouples your test name from implementation details
👉 If implementation (the how) changes, or delivery mechanism changes, the test name should not care and still represent valid behavior under test. You should not have to go and rename your test every time your implementation details change. If you do, your tests are coupled and that's a smell.
3️⃣ Acts as human readable documentation that tells part of the story of your codebase, the slice of value that was delivered in this part of the application
👉 Being able to read a describe and its underlying test names should be like talking to a person, where your eyes do not have to trip up or over technical terms to decipher the meaning of the test. You should be able to read a describe and test names within seconds and know what that area of the app is already doing.