📖 TLDR: This is a REX, not a flame post. We chose integration tests over unit tests for a more efficient and startup-friendly approach. It’s not perfect, but it works. We’re happy.
A very long time ago I stumbled on this interview of Dan Abramov. I was already using a lot of functional tests on my previous project, and wondered, do I really need anything else?
While there are dozens of “get to 100% coverage” articles, I took the decision to try a new approach: No unit tests!
It worked well for us, but in a recent interview, I was asked “what’s your unit test code coverage percentage?“, after explaining our strategy, the candidate was very surprised by this concept and we talked about it for a few minutes.
He asked me to write an article about it, so here we are! 😅
Here is my opinion, based on my personal experience:
What’s a codebase without tests ?
- A codebase where you don’t have any confidence you just broke something with a new piece of code
- A codebase where you don’t have any confidence that you fixed an issue
- A codebase where making changes makes you nervous
So to sum-up, a codebase where any change takes a lot of time, where agility is low and where feature ship slowly. Time is the issue at the end.
For me, tests are made to change this, to reach a state where:
- A codebase you have a strong confidence you did not break anything important with a new piece of code
- A codebase where you have a absolute confidence that you fixed an issue
- A codebase where changes flow and feels simple
Static Tests
You can use toolings to detect basic errors like typos and syntax. It’s well documented, not long to set up and catches a lot of small mistakes.
Unit Tests
You verify that functions are doing what they are meant to do, without external influence. Using patterns like dependency injection to avoid testing multiple behaviors at the same time.
Integration / Functional Tests
You verify a whole range of behaviors that fits a real use case. Usually limited to parts of an application / infrastructure.
End to End Tests
You verify that the whole application: database, server and client works, by simulating an user usage. In our case, simulating clicks in a browser.
TIME ! All our choices have been made to allow the programmer to be efficient.
Refactoring
In a startup, you don’t know for how long the code will be in place. Maybe in two months we will trash the whole feature? Maybe we will change its purpose?
We must be nimble, big changes must be possible in the smallest amount of time, without a big loss of quality. We must find a good equation between speed and regressions.
Understanding the use cases
They were useless because they didn’t actually correspond to anything that the user would see. And also, if you read it like three years later, you’re like, okay, I understand what this test is destined for in terms of this module, but why is this the behavior? Like what does it, what is, what is the problem it was trying to solve?
Focusing on user stories help to understand what the test is in charge of. It clearly states what’s the usage of the API and why it’s being used.
You trade: “The listener must be able to broadcast the signal with the metadata”
For: “The user must be able to subscribe to the service and get a valid token to identify himself after”.
Way faster to write
In our case, it’s mostly writing a single GraphQL query, and testing the result while the whole chain will be used:
The HTTP server, Express, Graphql, Sequelize, Postgres… everything.
function projects() {
return `/api/graphql?query=
quer