Where this came from
A few years ago I joined a team where one developer named his unit tests in a strange way. Every name read like "GIVEN something WHEN something happens THEN something is true", keywords in capital letters and all.
I'm honestly not sure he knew why he was doing it. Given/When/Then comes from behaviour-driven development (BDD), and people usually use it for acceptance tests, not for unit tests. Maybe he mixed up BDD and TDD. Maybe he saw it somewhere and liked the look of it. I never found out.
But the longer I worked with those tests, the more I noticed that there was something useful in them. Every test had the same shape. And even if the shape had been mediocre, it would still have beaten what most codebases have, which is no structure at all.
So I kept it. Over time I turned it into a small set of strict rules, and I have used it in production for many years, in different projects. It has given me a lot of value, more than its slightly silly look suggests.
I know how it looks. Shouting GIVEN, WHEN and THEN at the start of every unit test feels like ceremony. This article is my attempt to show you why it isn't, and to give you a reference you can come back to while you write your next test. The reasoning comes first, then the rules, then a lot of examples. There is a cheat sheet at the end.
Why borrow BDD words for unit tests?
Given/When/Then was invented to describe behaviour for people who don't read the code. Product owners, testers, the people writing acceptance criteria.
Now think about who reads the name of a failing unit test. Very often it's someone who hasn't read that code either: a new teammate, you a year from now, or anyone whose unrelated change happened to break it. It's the same reader with the same problem. They need to understand the behaviour without opening the implementation.
So this isn't BDD and TDD mixed up by accident. It's BDD's sentence used on TDD's tests, on purpose.
The one line you get
Picture this. You've just joined a project, or come back to one after a year away. You change something small and run the tests. Somewhere in code you've never read, a test you've never seen turns red, and the terminal gives you one line.
That line is all you have. Here is the kind of thing it usually says. These are the kind of names I've seen in all sorts of codebases.
FAIL test1 FAIL testLogin FAIL returns the expected result for the admin user FAIL handles the error case correctly FAIL updates the state after refresh FAIL saving a profile with a missing postcode after the address step was skipped on mobile still persists and does not throw The first two are obviously bad. The rest look reasonable: they're real sentences, and someone clearly tried. But none of them says what situation was tested or what was supposed to happen, so you can't do anything with them without opening the test.
Whichever one you get, you end up doing the same thing. You open the file, find the setup, then find the helper the setup calls. You read the assertions. Then you try to reconstruct what the author meant, and whether your change broke a real promise or just an outdated test. When it's a test you wrote last week, that's annoying. When it's a stranger's test in a suite of thousands, after a change you were sure was harmless, it can eat an afternoon.
The real problem isn't that the name is too short or too long, or that it's in camelCase instead of plain sentences. The problem is that to understand the name, you have to dive into context you don't have. That dive is the expensive part.
What I want instead: zero dive
I want to read the failing line and already know what's going on. No opening files, no hunting for the setup.
For that, the name has to answer four questions:
- What is under test? The class, module, function or screen. This tells you which file to open.
- What was the situation? The state things were in.
- What happened? The action or event.
- What should have been true afterwards? The promise that just broke.
GIVEN, WHEN and THEN are simply slots for those answers. GIVEN holds the subject. WHEN holds the situation and the action. THEN holds the expectation. Here's a whole failing line:
GIVEN ShoppingCart WHEN an item is added to an empty cart THEN the total is its price You know which file this test lives in, what was set up, what was done, and what went wrong. You can probably guess whether your change or the test is at fault before you open anything.
The second win is quieter. When every name in every file has the same shape, you stop reading how this particular author likes to phrase things. You just read what broke. The file you wrote yesterday and the one somebody wrote three years ago look the same.
That's the promise of this convention. Every test has a shape you can read at a glance, and no test makes you dive.
Part 1: The structure
One GIVEN per file, wrapping everything
The outermost group in a test file is the only GIVEN, and it names the thing under test. Every other test in the file lives inside it.
GIVEN ShoppingCart ...every test in the file... One file, one subject, one GIVEN. Never two at the top level, and never one nested inside another.
The subject doesn't have to be a class. It's whatever the file tests. For a file of loose helper functions, the subject is the module, in plain words: GIVEN date helpers, and each WHEN describes what's being done rather than which function is called. A function big enough to deserve its own test file can be the subject on its own: GIVEN formatDuration. What you shouldn't need is two subjects in one file. If you can't name a single subject for a test file, that's your hint to split it, and a source file with several classes in it is usually asking for the same thing.
You might wonder why the GIVEN is needed at all, when most test runners already print the file and line of a failure. The path tells you where the test lives, but not what it's about. And the GIVEN is part of the test's name, so it travels everywhere the name goes: CI summaries, test reports, the test tree in your IDE, a failure message pasted into a chat. Many of those show the name without the file. With the GIVEN in place, the sentence is complete on its own.
It also makes the output easy to scan. Every line from the file starts with the same GIVEN, so your eye learns to skip it and goes straight to the part that differs.
Every test has a WHEN and a THEN
WHEN is what happened. THEN is what you can observe afterwards.
GIVEN ShoppingCart WHEN an item is added THEN the total includes its price WHEN the last item is removed THEN the cart is empty A WHEN without a THEN doesn't tell you what the test checks. A THEN without a WHEN is a fact with no cause. Both are out.
The starting state goes into the WHEN
There's no separate keyword for "in this state". The state is simply part of the WHEN sentence.
WHEN an item is added to an empty cart THEN the total is its price WHEN the only item is removed THEN the cart is empty WHEN a discount is applied to a cart under the minimum THEN it is refused You might be tempted to use a second GIVEN for "an empty cart". Don't. GIVEN already has a job, and the WHEN sentence has plenty of room. When the runner prints the full path, a test run reads like a list of promises:
GIVEN ShoppingCart WHEN an item is added to an empty cart THEN the total is its price GIVEN ShoppingCart WHEN the only item is removed THEN the cart is empty Write the THEN even when it's obvious
Sometimes the THEN feels like it just repeats the WHEN:
WHEN an item is added to the empty cart THEN the cart has the new item It's tempting to drop it. Please don't. The whole point is that the terminal tells you what was checked, and a test without a THEN can't be judged from the terminal.
Instead, take the repetition as a hint. A THEN that only restates the action usually means the test isn't checking much. Ask yourself what else should be true, and say that:
WHEN an item is added to the empty cart THEN the total is the item's price AND joins two conditions, or two results
AND is the fourth keyword, and the easiest one to misuse. It joins two things of the same kind: two parts of a WHEN, or two parts of a THEN. It never connects a WHEN to a THEN, and it never replaces either of them.
WHEN ... AND ...: two conditions
Sometimes an action only matters in combination with another fact:
GIVEN SyncScheduler WHEN the app resumes after a long time in the background AND both sync options are enabled THEN a full sync is scheduled WHEN the app resumes after a long time in the background AND only one sync option is enabled THEN only that sync is scheduled The two tests differ by exactly one line, so a failure tells you which combination broke. Don't squeeze both conditions into one long sentence like "WHEN the app resumes after a long time in the background with both sync options enabled". The second condition gets buried in the middle, and the reader has to dig it out.
AND also works for two events that happen in order:
WHEN the first update arrives before the server confirms AND the second update arrives after it confirms THEN both are saved to the same day THEN ... AND ...: two results
Try to avoid AND after THEN. It usually means the test checks too many things at once, and a test that checks two things is usually better off as two tests:
GIVEN Checkout WHEN payment fails THEN the order stays pending WHEN payment fails THEN the customer is told why Now a failure tells you exactly which promise broke.
Sometimes splitting doesn't make sense, because the two results are really two halves of one promise, and a half-kept promise is no better than a broken one. (Or, let's be honest, sometimes you're just too lazy to split it. Boo.) In that case, join them with AND:
GIVEN Checkout WHEN payment fails THEN the order stays pending AND the customer is told why Practical notes
The rules above cover most tests. What follows are the situations you'll run into once you use them every day.
Tests that share a WHEN go into a WHEN group
Sometimes several tests do the same thing and check different results, like the two payment tests above. Don't repeat the WHEN in every name. Make it a group, and let the tests inside be just THEN.
GIVEN ShoppingCart WHEN a discount code is applied THEN the total drops by the discount THEN the code is shown on the receipt THEN applying it again changes nothing This is the only kind of nesting allowed. A WHEN group can hold THEN tests or more WHEN groups, but never a GIVEN.
It's also what keeps names short. One action, many facts about it, and the nesting follows exactly that shape.
Watch the spaces in long names
Long names are usually split in the source code into several string pieces, one per keyword:
'WHEN the app resumes after a long time in the background ' 'AND both sync options are enabled ' 'THEN a full sync is scheduled' Start every THEN and AND on a new piece, and end every piece before it with a space. Forget the space, and the runner happily prints "in the backgroundAND both sync options". This is a perfect job for a linter, because reviewers won't notice it until the output looks broken.
Part 2: The words between the keywords
Nothing in this part is specific to GIVEN, WHEN and THEN. It's plain good naming. But test names are the place where people tend to abandon good naming first, so it's worth saying out loud.
Describe the intention, not the code
A very common habit is to take the code and simply translate it into the test name. The GIVEN holds the class name, which is fine. But then the WHEN says which function was called, and the THEN says what it returned:
GIVEN Authentication WHEN logout is called THEN true is returned GIVEN Calculator WHEN addPercentage is called THEN a number is returned It's accurate, and it tells you nothing. These names read exactly like the code they test, so you still have to read the code to understand them. Instead, write what the user is trying to do and what should happen as a result:
GIVEN Authentication WHEN the user logs out THEN all their data is cleared from the local database and storage GIVEN Calculator WHEN the user adds a percentage to a number THEN the number increased by that percentage is returned There's a practical reason for this, not just a readable one. Function and class names change all the time during refactoring. The intention behind them, the use case, almost never does. A name that describes the intention survives the refactor. A name that describes the code has to be rewritten, or worse, it isn't rewritten and quietly becomes wrong.
Sometimes there is no user to talk about. Some code exists only to serve other code: a mapper, an adapter, a wrapper around a cache. That's fine, and technical words are fine there too. The line isn't between business words and technical words. It's between describing what the code does and quoting the code itself.
A test for a mapper can talk about responses, fields and defaults. It just shouldn't recite function names and constants. Instead of:
WHEN fromJson is called with a null avatarUrl THEN avatarUrl equals DEFAULT_AVATAR write:
WHEN the server sends a profile without an avatar THEN the profile gets the default avatar So whatever you're testing, leave the code symbols out of the name: function names, module paths, enum values, constants. The only real code name in a test name is the subject in the GIVEN, and it's there on purpose.
If the subject is a real type, refer to it instead of typing it
A string can drift away from the code it describes. Imagine a group called GIVEN StepsTotalsCubit, and a year ago someone renamed the class to StepsCubit. The string still reads perfectly well, so nobody notices, and the name quietly lies for months.
If your language can turn a type into a string, let it do the work:
| Language | Subject as a reference |
|---|---|
| Dart | 'GIVEN $StepsCubit' |
| C# | $"GIVEN {nameof(StepsCubit)}" |
| Kotlin | "GIVEN ${StepsCubit::class.simpleName}" |
| Python | f"GIVEN {StepsCubit.__name__}" |
Now, when someone renames the type, every test name follows. If someone misspells it, the test file fails to compile or load.
If your language can't do this, or the subject isn't a single type, just use plain words: GIVENdate helpers. Either a real reference or honest prose, never something that only looks like a class name.
Enforce the rules with a linter
Rules that only live in a document get forgotten. Rules checked by a linter stay.
I have two codebases in one project. One has a linter that fails the build when a test breaks these rules. The other doesn't. In the first, about 9 out of 10 test groups follow the rules. In the second, almost none do.
Even a linter isn't perfect. Mine had a bug: it allowed a GIVEN inside another GIVEN. When I checked, there were 107 of them. Wherever the linter is wrong, the tests end up wrong too.
So if you use this convention, write a linter for it. It should check that:
- each test file has one top-level group, and it starts with GIVEN
- every nested group starts with WHEN
- every test starts with WHEN, or with THEN if it's inside a WHEN group
- there's a space before every THEN and AND
- the GIVEN uses a type reference instead of a hand-typed class name, where the language allows it
It's a few hundred lines of code, and it saves you from explaining these rules to every new teammate.
Good examples
Here are three complete files, written as the test runner would print them. They come from different domains, but they all have the same shape.
A value object:
GIVEN Money WHEN two amounts in the same currency are added THEN the result is their sum WHEN two amounts in different currencies are added THEN it is refused WHEN an amount is split three ways THEN the parts add back up to the original THEN the remainder goes to the first part A service, with a shared action:
GIVEN PasswordResetService WHEN a reset is requested for a known email THEN a single-use token is stored THEN an email with the token is queued THEN the token expires in one hour WHEN a reset is requested for an unknown email THEN nothing is stored and nothing is sent WHEN a token is used twice THEN the second use is refused A screen, with a two-part condition. In the source code the subject is a type reference, and the runner prints its name:
GIVEN DailyStepsPage WHEN the page opens with no steps recorded THEN the counter shows zero WHEN the page opens with steps recorded THEN the counter shows today's total WHEN the add button is tapped AND the goal is already reached THEN the celebration is shown once Look at how much you can tell without opening a single file. You know which promise is broken. You know whether the subject is a class you just touched.
You can even see what isn't tested. Nothing in the password reset file covers a token used after it has expired, and the gap is visible from the names alone.
Bad examples
An old name with keywords glued on
WHEN renders celebration message THEN The test used to be called "renders celebration message", and the keywords were stuck around it to keep a rule happy. Now the THEN is empty, and the WHEN is actually a result.
Better:
WHEN the weight goal is reached THEN the celebration message is shown The result hiding in the WHEN
WHEN an item is added and the total updates THEN no error is thrown "The total updates" is the thing actually worth testing, and it has been demoted to a condition. Meanwhile the THEN, "no error is thrown", is the least interesting fact about this test.
Better:
WHEN an item is added THEN the total includes its price Cheat sheet
- One GIVEN per file. It names the subject and is never nested.
- Every test has a WHEN and a THEN.
- The starting state goes into the WHEN, not into a second GIVEN.
- Write the THEN even when it's obvious. If it repeats the WHEN, check something more.
- Use AND to add a second condition to a WHEN.
- Avoid AND after THEN. Split it into two tests, unless the two results are one promise.
- Tests that share a WHEN go into a WHEN group. That's the only nesting allowed.
