A spec describes what the program should do. We will use a small to-do app to turn one of its requirements into a test. That test runs on every change, so the same covered behaviour does not need another manual check each time. We will write the requirement as a story, split it into given, when and then, and write it in pytest. Then we will test two features together, because each can pass its own tests while the combination fails.
Imagine reviewing these four requirements for a small to-do app. Two are met, one needs a decision about scope, and one is not met. Whether clearing completed items should ask for confirmation is a scope decision, but once decided, it is a requirement you could specify and test.
That manual review happened once. It tells you what was true at one moment. The next change needs another check of those requirements.
Now look at the first three clauses. Each one says something the program does, and something a program can therefore go and check. That check can run on every change, instead of once.
Here is a capability we want our to-do app to have. The user should be able to save the to-do list and load it again later, so that closing the app does not lose it. In software teams, a requirement written from the user's side like this one is called a story. Here we express the story as one sentence about what the user does and what should happen.
A story names who does something, what they do, and what should happen next. It is written in your words, the words you would use to describe the program to a friend.
The story is written from outside the program. It names no function, file format or framework, so the implementation can change while the requirement stays the same. The list could be saved to a file or to a database, and the story would not change. The test we write from it still has to choose a concrete way into the program and concrete data to try. Those choices belong to the test, not to the story.
Turning that story into a test is a matter of cutting it into three parts. The first part sets up the situation the story assumes, which here is a to-do list with three items: Buy milk, Call mom and Pay rent.
The second part performs the action. Save the list, then load it into a fresh app, the way it would be loaded after the app was closed.
The third part checks the expected outcome. The loaded list is the same list: the same items, in the same order, and nothing added or missing. The word same, in the story, is doing real work, and the assertion has to carry it. A test may perform several actions and check several results when the scenario needs them. What keeps it focused is that it checks one behaviour, so a failure points at one thing to investigate.
These three parts are called given, when and then, in that order. They come from behaviour-driven development. That practice includes tools with English-like syntax and glue code that turns the steps into execution. Here we use the same three-part structure directly in an ordinary test.
Here are the same three parts written as code. The example is in Python, run by pytest. The first line calls list with, a small helper written for these tests. It builds a list of the three items, all of them still open. The second line saves the list and loads the saved text back. Load builds a new list from that text alone, which is what a fresh app would do. The test calls save and load directly, as plain functions, with no browser. It checks the saving and loading code itself. It does not show that the Save button on the page reaches that code, which is what a browser test checks. The last line compares the loaded list with the original, item by item. So it checks every item, everything recorded about each one, and their order. Here the order is part of the requirement, because another clause puts new items at the bottom of the list, and loading must not rearrange them.
The test's name says that a saved list loads back the same, closely matching the story. That makes it easier to connect a failing test to the behaviour you promised. A name about a JSON round trip describes the implementation, but says less clearly what the user should get back.
Here is how a failure looks. The report says the original list has one more item than the loaded one, Pay rent. The last item was lost on the way, which is a concrete wrong result to investigate, starting with the code that reads the saved list back. The report also names the test, so you can connect the missing item with the requirement that the same list comes back.
Let's go back to our to-do app. Besides saving and loading, it has another feature. You can complete an item by clicking its checkbox, and that marks the item as done. The Active filter uses that mark. It shows only the items that are not done yet. Completing an item has its own test, and that test passes. The save and load test we just wrote passes too. Notice that the save and load test used a list where nothing was done. Every item in it was still open.
Now let's use the two features together, the way a user would. First, the user clicks the checkbox next to Buy milk, so Buy milk is done. Then the user saves the list. Later, the user opens the app again and loads the list. Finally, the user chooses the Active filter to see what is left to do.
Buy milk was done before the list was saved. So the Active filter should show only Call mom. Instead, the app shows both Buy milk and Call mom. The saved file explains why. For each item, it records the title and nothing else. It does not record whether the item was done. So when the list is loaded, every item comes back as not done, and Buy milk shows up again as something left to do.
Whether an item is done is part of the state of the to-do app, not just its list of titles. In the code, each item keeps this mark in a field called the done flag. When the user saves, the app has to save the done flag too. A good way to find problems like this one is to follow a piece of state through the app. For the done flag, clicking the checkbox sets it, and the Active filter reads it to decide what to show. Saving and loading have to carry it in between, and here, saving leaves it out. Neither test ever tried this path, because the save and load test never saved an item that was done.
Here is a test for this combination, in the same "given", "when" and "then" shape. It starts with two open items, Buy milk and Call mom. It completes Buy milk, which is what clicking its checkbox does. Then it saves the list and loads it back into a new list, just as reopening the app would. The last line checks what the Active filter would show. It calls visible items, the function that decides which items a filter shows. We expect Call mom alone. This test has several steps, but it checks one result. Run against the current app, it fails, because Buy milk comes back as not done.
The fix is to make save write the done flag for each item, and make load read it back. Then the test passes. Keep the test. The next time someone changes how the app saves its list, or adds a new field to each item, it will catch a done flag that goes missing again.
When writing a spec, look for clauses with an outcome you can state precisely. Each of these describes behaviour for which a test can set up a case and compare the result with an expectation.
Taste is different. Clean, easy on the eye and quick are judgments about how the app feels, not facts about what the program does. A test can settle whether only unfinished items are shown. No test can settle whether the list looks clean. Those judgments need review of the experience, even when some supporting properties can be checked automatically. A judgment can still lead to a measurable target. Feels quick might become a rule that a new item appears within a tenth of a second. A test can check that number, and someone still has to use the app to judge whether it feels quick.
Keep those judgment-based requirements in the spec too. They tell you what to review as the interface changes. Distinguishing the two kinds while you write helps you plan both the automated checks and the manual review.
A behaviour clause in your own words can become a named test. Once it runs after each change, you have a repeatable way to check that requirement and can spend your review time on what still needs judgment.
Features can pass their own tests and still fail together. When features share state, test their combined behaviour, and keep that check for later changes.
You can now instruct the agent to write better tests. Give it a behaviour clause from your spec, and ask for a named test that checks it. And while it's doing that, call your mom and buy some milk. See you in the next video.