TDD Is Still
the Goat
Why You Shouldn't Bother with the Latest Craze.
Devin Fitzsimons
Senior Engineer, AI · CompanyCam
TDD Is Still
the Goat
Why You Shouldn't Bother with the Latest Craze.
Devin Fitzsimons
Senior Engineer, AI · CompanyCam






What made TDD great to begin with?
- Planning ahead, without getting stuck.
- Confidence as you make changes.
- You could make a lot of money selling a book.
- The red-green-refactor cycle.
- Commits had a natural rhythm.
What made TDD great to begin with?
- Planning ahead, without getting stuck.
- Confidence as you make changes.
- You could make a lot of money selling a book.
- The red-green-refactor cycle.
- Commits had a natural rhythm.
What made TDD great to begin with?
- Planning ahead, without getting stuck.
- Confidence as you make changes.
- You could make a lot of money selling a book.
- The red-green-refactor cycle.
- Commits had a natural rhythm.
What made TDD great to begin with?
- Planning ahead, without getting stuck.
- Confidence as you make changes.
- You could make a lot of money selling a book.
- The red-green-refactor cycle.
- Commits had a natural rhythm.
What made TDD great to begin with?
- Planning ahead, without getting stuck.
- Confidence as you make changes.
- You could make a lot of money selling a book.
- The red-green-refactor cycle.
- Commits had a natural rhythm.
What makes coding with LLMs great?
- They do the annoying stuff.
- It's fast.
- The economy doesn't collapse because we all realize we should go back to coding by hand.
- Less syntax anxiety.
- You're open to using more languages.
What makes coding with LLMs great?
- They do the annoying stuff.
- It's fast.
- The economy doesn't collapse because we all realize we should go back to coding by hand.
- Less syntax anxiety.
- You're open to using more languages.
What makes coding with LLMs great?
- They do the annoying stuff.
- It's fast.
- The economy doesn't collapse because we all realize we should go back to coding by hand.
- Less syntax anxiety.
- You're open to using more languages.
What makes coding with LLMs great?
- They do the annoying stuff.
- It's fast.
- The economy doesn't collapse because we all realize we should go back to coding by hand.
- Less syntax anxiety.
- You're open to using more languages.
What makes coding with LLMs great?
- They do the annoying stuff.
- It's fast.
- The economy doesn't collapse because we all realize we should go back to coding by hand.
- Less syntax anxiety.
- You're open to using more languages.
What makes coding with LLMs great?
- They do the annoying stuff.
- It's fast.
- The economy doesn't collapse because we all realize we should go back to coding by hand.
- Less syntax anxiety.
- You're open to using more languages.
What makes coding with LLMs terrible?
import { useState } from "react";
type Project = { id: number | string; name: string };
function useProjects({ projects: incomingProjects }: { projects: Project[] }) {
// Initialize the project states.
const [isProjectOpen, setIsProjectOpen] = useState(incomingProjects.map(() => false));
const [isCurrentProject, setIsCurrentProject] = useState(incomingProjects.map(() => false));
const [isProjectEnabled, setIsProjectEnabled] = useState(incomingProjects.map(() => true));
const [isProjectValid, setIsProjectValid] = useState(incomingProjects.map(() => false));
// Prepare the projects so they are ready.
const readyProjects = incomingProjects.map(project => ({ ...project, ready: true }));
// Reduce the ready projects into a list of projects.
const listedProjects = readyProjects.reduce<Project[]>(
(projects, project) => [...projects, { ...project }], []
);
// Filter the listed projects by their project ID type.
const numberedProjects = listedProjects.filter(project => typeof project.id === "number");
// Map the projects together and cast them as projects.
const projects = readyProjects.map((project, index) => ({
...project, ...numberedProjects[index],
})) as Project[];
// Handle validating the project by updating the project states.
const handleValidateProject = (projectId: number) => {
// Create a map of the projects, then get the unique projects from the map.
const projectMap = new Map(projects.map(project => [project.id, { ...project }]));
const uniqueProjects = Array.from(new Set(Array.from(projectMap.values())));
setIsProjectValid(uniqueProjects.map(project => project.id === projectId ? true : true));
setIsCurrentProject(uniqueProjects.map(project => project.id === projectId));
};
// Return the projects and the project states with the project handler.
return { projects: projects.map(project => ({ ...project })),
isProjectOpen, isCurrentProject, isProjectEnabled, isProjectValid, handleValidateProject };
} What makes coding with LLMs terrible?
import { useState } from "react";
type Project = { id: number | string; name: string };
function useProjects({ projects: incomingProjects }: { projects: Project[] }) {
// Initialize the project states.
const [isProjectOpen, setIsProjectOpen] = useState(incomingProjects.map(() => false));
const [isCurrentProject, setIsCurrentProject] = useState(incomingProjects.map(() => false));
const [isProjectEnabled, setIsProjectEnabled] = useState(incomingProjects.map(() => true));
const [isProjectValid, setIsProjectValid] = useState(incomingProjects.map(() => false));
// Prepare the projects so they are ready.
const readyProjects = incomingProjects.map(project => ({ ...project, ready: true }));
// Reduce the ready projects into a list of projects.
const listedProjects = readyProjects.reduce<Project[]>(
(projects, project) => [...projects, { ...project }], []
);
// Filter the listed projects by their project ID type.
const numberedProjects = listedProjects.filter(project => typeof project.id === "number");
// Map the projects together and cast them as projects.
const projects = readyProjects.map((project, index) => ({
...project, ...numberedProjects[index],
})) as Project[];
// Handle validating the project by updating the project states.
const handleValidateProject = (projectId: number) => {
// Create a map of the projects, then get the unique projects from the map.
const projectMap = new Map(projects.map(project => [project.id, { ...project }]));
const uniqueProjects = Array.from(new Set(Array.from(projectMap.values())));
setIsProjectValid(uniqueProjects.map(project => project.id === projectId ? true : true));
setIsCurrentProject(uniqueProjects.map(project => project.id === projectId));
};
// Return the projects and the project states with the project handler.
return { projects: projects.map(project => ({ ...project })),
isProjectOpen, isCurrentProject, isProjectEnabled, isProjectValid, handleValidateProject };
} also the waiting.
What makes coding with LLMs terrible?
import { useState } from "react";
type Project = { id: number | string; name: string };
function useProjects({ projects: incomingProjects }: { projects: Project[] }) {
// Initialize the project states.
const [isProjectOpen, setIsProjectOpen] = useState(incomingProjects.map(() => false));
const [isCurrentProject, setIsCurrentProject] = useState(incomingProjects.map(() => false));
const [isProjectEnabled, setIsProjectEnabled] = useState(incomingProjects.map(() => true));
const [isProjectValid, setIsProjectValid] = useState(incomingProjects.map(() => false));
// Prepare the projects so they are ready.
const readyProjects = incomingProjects.map(project => ({ ...project, ready: true }));
// Reduce the ready projects into a list of projects.
const listedProjects = readyProjects.reduce<Project[]>(
(projects, project) => [...projects, { ...project }], []
);
// Filter the listed projects by their project ID type.
const numberedProjects = listedProjects.filter(project => typeof project.id === "number");
// Map the projects together and cast them as projects.
const projects = readyProjects.map((project, index) => ({
...project, ...numberedProjects[index],
})) as Project[];
// Handle validating the project by updating the project states.
const handleValidateProject = (projectId: number) => {
// Create a map of the projects, then get the unique projects from the map.
const projectMap = new Map(projects.map(project => [project.id, { ...project }]));
const uniqueProjects = Array.from(new Set(Array.from(projectMap.values())));
setIsProjectValid(uniqueProjects.map(project => project.id === projectId ? true : true));
setIsCurrentProject(uniqueProjects.map(project => project.id === projectId));
};
// Return the projects and the project states with the project handler.
return { projects: projects.map(project => ({ ...project })),
isProjectOpen, isCurrentProject, isProjectEnabled, isProjectValid, handleValidateProject };
} also the waiting.
Also we're dumber.
To review
- Developers are burned out.
- Coding models write awful code.
- We don't remember what we're doing.
- We have to wait around, so we lose track of what we're doing.
- One-shotting never works.
- We build software factories, and they never work.
- We spend more time introspecting our software factories and trying to do stuff.
To review
- Developers are burned out.
- Coding models write awful code.
- We don't remember what we're doing.
- We have to wait around, so we lose track of what we're doing.
- One-shotting never works.
- We build software factories, and they never work.
- We spend more time introspecting our software factories and trying to do stuff.
To review
- Developers are burned out.
- Coding models write awful code.
- We don't remember what we're doing.
- We have to wait around, so we lose track of what we're doing.
- One-shotting never works.
- We build software factories, and they never work.
- We spend more time introspecting our software factories and trying to do stuff.
To review
- Developers are burned out.
- Coding models write awful code.
- We don't remember what we're doing.
- We have to wait around, so we lose track of what we're doing.
- One-shotting never works.
- We build software factories, and they never work.
- We spend more time introspecting our software factories and trying to do stuff.
To review
- Developers are burned out.
- Coding models write awful code.
- We don't remember what we're doing.
- We have to wait around, so we lose track of what we're doing.
- One-shotting never works.
- We build software factories, and they never work.
- We spend more time introspecting our software factories and trying to do stuff.
To review
- Developers are burned out.
- Coding models write awful code.
- We don't remember what we're doing.
- We have to wait around, so we lose track of what we're doing.
- One-shotting never works.
- We build software factories, and they never work.
- We spend more time introspecting our software factories and trying to do stuff.
To review
- Developers are burned out.
- Coding models write awful code.
- We don't remember what we're doing.
- We have to wait around, so we lose track of what we're doing.
- One-shotting never works.
- We build software factories, and they never work.
- We spend more time introspecting our software factories and trying to do stuff.
To review
- Developers are burned out.
- Coding models write awful code.
- We don't remember what we're doing.
- We have to wait around, so we lose track of what we're doing.
- One-shotting never works.
- We build software factories, and they never work.
- We spend more time introspecting our software factories and trying to do stuff.
What do we do?
What do we do?
We TDD.
Commands
A little-known feature in AI coding agents: custom slash commands.
Like skills, but they work reliably because you run them.
-
/TDD-plan -
/TDD -
/TDD-update -
/TDD-learn
Commands
A little-known feature in AI coding agents: custom slash commands.
Like skills, but they work reliably because you run them.
-
/TDD-plan -
/TDD -
/TDD-update -
/TDD-learn
Commands
A little-known feature in AI coding agents: custom slash commands.
Like skills, but they work reliably because you run them.
-
/TDD-plan -
/TDD -
/TDD-update -
/TDD-learn
Commands
A little-known feature in AI coding agents: custom slash commands.
Like skills, but they work reliably because you run them.
-
/TDD-plan -
/TDD -
/TDD-update -
/TDD-learn
Commands
A little-known feature in AI coding agents: custom slash commands.
Like skills, but they work reliably because you run them.
-
/TDD-plan -
/TDD -
/TDD-update -
/TDD-learn
/TDD-plan
# TDD-plan
We're going to build this feature: $ARGUMENTS. Create a plan for a different coding agent to implement. Do not implement the feature.
## Build the plan with me
1. Interview me about the feature. Ask focused questions about the behavior I want, what is out of scope, and how we'll know it works. Do not invent requirements.
2. Look up relevant files, existing patterns, tests, and repository instructions. Use what you find to make your next questions more specific.
3. Once we have enough detail, identify blockers, dependencies, coding standards, and testing constraints. Work through them with me; do not silently decide.
4. Build up the plan as we talk. Include agreed behavior, relevant files, constraints, and an ordered list of small, testable steps. Call out unresolved questions.
5. Ask me to approve the completed plan before handing it to another coding agent.
6. Save the plan to `plans/feature-name.md`.
End the plan with the following section, copied verbatim:
## Way of working
We work on a strict red-green-refactor cycle. Default to writing code, not proposing it. Each test should test one thing.
### Red
1. Write the fucking test. One reasonable failing test.
2. Give the human the command to run it, such as `cocam test path/to/file`. Do not run it yourself.
3. Wait for confirmation that it failed in the expected way.
### Green
4. Write only the minimal code to pass that test. No extra features.
5. Give the human the test command again. Do not run it yourself.
6. Wait for confirmation that it passes.
### Refactor
7. Bring up refactoring ideas now.
8. Agree on the changes with the human, then make the agreed changes.
9. After refactoring, give the human the test command. Wait for confirmation that it passes.
10. Repeat one test at a time.
Periodically give the human the lint command. Do not run it yourself. /TDD
# TDD
Read the plan at $ARGUMENTS.
Follow the plan and its Way of working. ← args is the path to the plan.
<!--
Here's an example of what my prompt in this file looks like.
-->
- Keep it short.
- Talk like a human.
- Feel free to curse.
- Ask one question at a time.
- Don't kiss my ass.
FYI, we're in a herdr session. /TDD-update
# TDD-update
Update the appropriate command based on what the human is telling you: $ARGUMENTS.
My OpenCode commands live at `~/.config/opencode/commands/`.
1. Read the appropriate command. If it's unclear which one to update, ask me.
2. Make only the requested changes. Don't rewrite the rest or add extra rules.
3. Briefly tell me what changed and whether you saved it or I need to apply it.
Then give me these three commands:
- /TDD
- /TDD-plan
- /TDD-learn /TDD-learn
# TDD-learn
Using the Anki CLI, save this to a flashcard: $ARGUMENTS.
1. Ask me about the front and the back of the flashcard.
2. Check that the flashcard makes sense: does the front ask a clear question, and does the back answer it?
3. Ask which deck to use: `companycamapi` or `companycam-mobile.dex`.
4. Save the flashcard using the Anki CLI. - This is personal to the developer.
- How do you want to learn and reinforce what you've learned?
My personal setup
- Multiple worktrees.
- TDD sessions.
- Exploring multiple work trees in herdr.