Thoughts On Testing
This is a draft!
I’m by no means a TDD purist. However tests are usually where I start most code I write.
In my career as a consultant I have talked to many developers who hate writing tests, and it’s usually for the same reasons:
They were thinking about them in the wrong way. They were using them wrong.
Done. Now let me add tests! Ugh.
It’s always harder to add a test after the fact. You ask yourself “It’s working, but how do I even write a test for this?”. Suddenly you think mocking is good idea (it very rarely is).
Wish Driven Development
also known as Readme-Driven-Development, Documentation-Driven-Development and so on.
The idea is to start writing documentation and code, just pretending certain functions and APIs exist. It’s a great way to draft out the surface of a module without worrying about implementation.
That’s pretty much what test-driven-development is, too. Just write a test, pretending a function/api exists, until you’re satisfied with the caller side of things.
Then… implement them.
You’ll likely go back and forth between test and implementation, realizing that you didn’t consider certain aspects, or that the implementation could be massively simplified if you just slightly adjust the API.
And at some point: it works. Congratulations, you’ve done TDD. Now go refactor your implementation, make it nice, keep the tests green.
That’s TDD.
Save. Click, Click, Click. Repeat. Automate!
Tests are first and foremost a form of automation.
Start implementation in your test
I do this a lot. Some languages even have first class support for this. And especially in code bases with a rotten testing environment this is a neat trick I tend to do:
I start the implementation inside the test and keep it isolated from the rest of the application. That way there’s nothing that gets in the way. No annoying orchestration. Just running a single test file.
// some-new-feature.test.ts
const store = [];
const addToStore = (input) => {
store.push(input)
}
describe('some new feature', () => {
it('does its thing', () => {
expect(store.length).toBe(0)
addToStore(input)
expect(store.length).toBe(1)
})
})
As a bonus: when you start importing things from the application, you’ll quickly spot weaknesses.
Avoid E2E, embrace the unit.
E2E tests are slow and flakey. Avoid them as much as possible. Unit tests are blazingly fast and reliable. Write them as much as possible.
That also means you need to write your implementation in a way that can be tests without using Playwright. Most of the time, even in complex UIs, things can be broken down to:
[event] -> someHandler(someData) -> [updated ui]
That’s why it’s a good idea to keep your view layer as dumb as possible, and keep business logic inside of small, unit-testable functions.