I've recently come across Fitness Function Driven DevelopmentOpen in a new tab. It's this new practice that seems to pair extremely well with Agentic Driven Development. It states we must have ways to validate that the project follows best practices, architecture and team conventions either computationally (mostly) or inferentially.
When we validate a project computationally, it means that there's code that runs checks deterministically. This in turn means that if we run them over and over again, if nothing changes, the result won't change either.
I'm the type of person that for checks really enjoys determinism, if not, is that really a check?
While inferential checks are how humans (or AI) perceive, for instance, a codebase. We can rely on feelings and opinions from developers about how robust the codebase is. Depends on whom you ask what you ask, or when you ask, you'll get different answers. Ergo it's not deterministic.
When I heard about Fitness Function Driven Development, I could barely contain my laughter, I told myself, wait, this is nothing new! Somehow, knowing about best practices and applying them has given me a head start about something that I've been doing for the past 10 years. Now, not only do we need to think about teaching humans best practices, AI needs to understand what are our best practices as a team too. And the best way to do it is with computational checks.
#How to approach FFDD
The first thing I want to approach about Fitness Function Driven Development is: testing.
“”Tests are essential in the AI era.
#E2E Testing
Now, if the project doesn't have a solid architecture, I start by adding E2E tests. I usually follow the Page Object Model plus a Flow Object. They pair extremely well.
First, I start defining the Page Object (PO):
import { expect, Page, test } from '@playwright/test'
class UserPo {
readonly selectors = {
user: 'Current user',
name: 'User name',
}
constructor(private readonly page: Page) {}
async open(): Promise<this> {
await this.page
.getByRole('button', { name: this.selectors.user })
.click()
return this
}
async getName(): Promise<string> {
return this.page
.getByRole('heading', { name: this.selectors.name })
.innerText()
}
}It has selectors and knows how to interact or retrieve with specific parts of a page. Then we have the Flow Object. It knows how to interact with multiple Page Objects to represent a flow of the application.
interface BookingUserFlowParams {
userPo: UserPo
bookingPo: BookingPo
}
class BookingUserFlow {
private constructor(
private readonly userPo: UserPo,
private readonly bookingPo: BookingPo,
) {}
static create({
userPo,
bookingPo,
}: BookingUserFlowParams): BookingUserFlow {
return new BookingUserFlow(userPo, bookingPo)
}
async execute(): Promise<void> {
await this.userPo.open()
const userName = await this.userPo.getName()
await this.bookingPo.selectUser(userName)
await this.bookingPo.book()
await this.bookingPo.expectBookingCreated()
}
}With this, we have a semantic, lean and effective test.
test('books a vehicle for the current user', async ({ page }) => {
const flow = BookingUserFlow.create({
userPo: new UserPo(page),
bookingPo: new BookingPo(page),
})
await flow.execute()
})Assertions are kept in the flow, since we might need to access data from different pages
#Integration tests
I usually work with integration tests. The way I handle it is through Vitest Browser Mode. To me, integration tests in the frontend usually mean testing complex component interactions, without the need to access the page via an url.
The browser mode loads them in the DOM.
Just like this:

The other use cases for integration tests are... use cases! When some use cases are a bit complex, I test them together.
describe('GetDestinationsQry', () => {
it('should get destinations', async () => {
const { getDestinationsQry, destinationRepository } = setup()
when(destinationRepository.findAll()).thenResolve([DestinationMother.europe()])
const result = await getDestinationsQry.handle()
expect(result).toEqual([DestinationMother.europe()])
})
})
function setup() {
// 1. Unit test
// const destinationRepository = mock<DestinationRepository>()
// 2. Integration test
// const destinationRepository = new DestinationInMemoryRepository()
const getDestinationsQry = new GetDestinationsQry(destinationRepository)
return { destinationRepository, getDestinationsQry }
}Given that I follow Inversion of Control and Dependency Inversion, making the switch is trivial.
Want to read more about these patterns? Check out this issue about SOLID.
#Unit tests
Unit tests are quite similar to the integration tests I've shown before, the difference is that I rely on mocks to have more control about the unit I'm testing.
I follow the F.I.R.S.T approach and I prefer having a setup function at the end of the file. To me, is more important to read what the tests do first rather than the mocks.
If some configuration is needed for the setup() I just pass it as a config object: setup({ mockRepository: true }).
To mock, I use @typestrong/ts-mockito. I really like it. Simple and elegant. The only drawback is that the owner has abandoned the original library. No one knows why. Anyway, the library was close to perfect anyway, it was just missing some bits that the typestrong org covered.
I use 3 to 4 different vitest.config files, one for each type of tests. For the files I use the following convention:
foo.unit.test.tsbar.it.test.tsbaz.e2e.test.tsbaz.arch.test.ts
#Architecture tests
The way I enforce architecture these days is through tests as well. I want to make sure I have some of the following checks:
- Max 300 lines per file
- High cohesion
- Layered (just like the name of this newsletter)
I use ArchUnitTS to turn layers into deterministic checks. Here is an illustrative example:
import { projectFiles } from 'archunit'
describe('Layers', () => {
it('domain should not know about any other layer', async () => {
const rule = projectFiles()
.inFolder('src/features/**/domain/**')
.shouldNot()
.dependOnFiles()
.inFolder('src/features/**/{application,infrastructure,delivery}/**')
await expect(rule).toPassAsync()
})
it('application should only know about the domain', async () => {
const rule = projectFiles()
.inFolder('src/features/**/application/**')
.shouldNot()
.dependOnFiles()
.inFolder('src/features/**/{infrastructure,delivery}/**')
await expect(rule).toPassAsync()
})
})#Linting and Formatting
To enforce lint rules, I have been using lately Biome rather than ESLint, since it's way faster and I can replace Prettier with it as well. Depending on the project, I configure a set of custom rules or expand on the recommended ones.
As for formatting, yeah, I'm one of those people that avoid semicolons. Hey, fewer tokens for AI to read.
The formatting and linting I run through a pre-commit. I use lefthook for that.
Depending on the project, I run some tests in the pre-commit or I move it to the pre-push commit. No need to waste CI time to discover you missed a console.log.
#Compiling
TypeScript is compulsory. I run compilation checks with the strictest config that I can have. Something like this:
{
"compilerOptions": {
// Environment (most modern)
"target": "esnext",
"moduleResolution": "bundler",
"skipLibCheck": true,
// Strictness beyond the (now default) strict family
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noImplicitOverride": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"allowUnreachableCode": false,
"allowUnusedLabels": false,
// Module discipline
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"erasableSyntaxOnly": true
}
}To simplify, I tend to compile for modern browsers, but it depends on the project.
#Inferential AI checks
I've been experimenting with inferential checks using custom Code Review Agents that check the code based on different lenses:
- Correctness
- Security
- Tests
- Architecture
- Performance
- Readability
Each lens is its own skill. The /review skill launches an agent from a fresh context. In full agentic programming mature projects, the review skill shouldn't be needed.
If we have a solid foundation, with skills, the harness and computational checks, reviewing AI-generated code using AI doesn't make too much sense; the code should already be pristine.
However, when completely changing the approach of a project, agents tend to weight more how things are currently done, rather than following skills. So the review skill makes sense then.
#Conclusion
Architectural Fitness Functions help tremendous AIs (and humans too!) to reach reproducible conventions, have a solid architecture in a deterministic way.
Here are some of the tools I've mentioned during this issue:
- PlaywrightOpen in a new tab
- Vitest Browser ModeOpen in a new tab
- @typestrong/ts-mockitoOpen in a new tab
- ArchUnitTSOpen in a new tab
- BiomeOpen in a new tab
- lefthookOpen in a new tab
- TypeScriptOpen in a new tab
P.S: Here, for you, one of my most precious skills: /interview