Testing is More Larger Than Just Writing Some Tests
Note
I am not a testing expert. I am merely sharing my thoughts on testing as a developer, in an article I would have appreciated when I began coding.
I started coding six years ago. Since then, I've heard a lot about testing, but I have seen few real and practical examples.
In many articles I read, tests look like this:
it('should return 2 when 1 + 1', () => {
expect(1 + 1).toBe(2)
})
This is far from real-world scenarios and does not help in understanding the true value of testing.
I have to admit, when I don't comprehend something, I avoid using it. Nevertheless, since I first encountered the concept of testing, it has lingered in my mind, prompting me to gather knowledge about it.
A Long Journey
I first heard about testing when I learned Vue 2 in 2019. In the tooling section, under the "Single File Component" page, I found a section dedicated to "Testing."
I think I read the beginning of the page but never went further. There were too many new concepts to grasp, too many tools to learn, and too many choices to make. At that time, I understood that front-end testing is by far the most challenging part of testing.
In 2020, with some friends, we developed a tutoring platform. The API server was built with Feathers.js 4. At that time, I was still grappling with testing. I remember looking for the testing page in the Feathers.js documentation and even reading it. Despite not fully understanding it, I wrote many tests. It was a real struggle because the distinction between unit tests and integration tests was unclear to me, and running them in a CI was very difficult. I was using MongoDB and Azure CI. Looking back, I think I wanted to endure pain. I also remember trying to achieve 100% coverage. I was so naive.
it('should patch', async () => {
expect.assertions(5)
const patchedResult: Department = await app
.service(serviceName)
.patch(result._id, anotherDepartment)
expect(patchedResult).toBeDefined()
expect(patchedResult).toHaveProperty('_id')
expect(patchedResult).toHaveProperty(
'name',
anotherDepartment.name.toLowerCase()
)
expect(patchedResult).toHaveProperty('createdAt')
expect(patchedResult).toHaveProperty('updatedAt')
})
it('should delete', async () => {
expect.assertions(5)
const deleteResult: Department = await app
.service(serviceName)
.remove(result._id)
expect(deleteResult).toBeDefined()
expect(deleteResult).toHaveProperty('_id')
expect(deleteResult).toHaveProperty('name', result.name.toLowerCase())
expect(deleteResult).toHaveProperty('createdAt')
expect(deleteResult).toHaveProperty('updatedAt')
})
But the more I added features to the platform, the more I realized the value of testing. It can ensure that a new feature does not break an existing one without the hassle of manually testing everything. This was particularly evident because Feathers.js has many intricate parts and services that can easily break when one part is modified.
export default {
before: {
all: [],
find: [],
get: [],
create: [checkData(checkDataOptions)],
update: [disallow()],
patch: [checkData(checkDataOptions)],
remove: [],
},
after: {
all: [],
find: [iff(isProvider('external'), pickResult())],
get: [iff(isProvider('external'), pickResult())],
create: [],
update: [],
patch: [],
remove: [],
},
}
The complexity of the app grew exponentially, feature after feature. This was more related to the architecture of Feathers.js, but combined with the challenges of writing efficient tests, it was a nightmare to maintain. Each change broke so many tests that I was unsure if the app was working or not.
From that point on, I decided the ability to test the app was a requirement for choosing a new stack. And this will be a long journey because in 2024, I'm still learning a lot but writing more tests than ever.
At the beginning of 2021, Strapi was gaining popularity, and I looked at it as a potential replacement for Feathers.js, still to build the tutoring platform. I was pleased to see that Strapi has a dedicated page for testing in the documentation.
So, I jumped in and created a new project with Strapi to get started and see if testing was effective.
Sadly, I immediately encountered a problem that never got resolved. Unable to run tests, I gave up and the tutoring platform was never migrated to Strapi, nor deployed to production. I learned a lot from this side project. Often, the journey is more important than the destination.
So let's recap:
- I only know JavaScript.
- Feathers.js has a complex architecture and poor documentation on testing.
- Strapi has good documentation on testing, but I was unable to run tests.
- I need to find a new side project to learn testing.
Some months later, I heard about AdonisJS. I don't remember how, but at that time, version 5 was in beta. It was a breath of fresh air because the documentation looked very good, and the framework was rich in features. Full of features, yes, but the testing part was not there yet. The core team released Japa a year later. It's a pleasant testing framework for Node.js.
Regardless, I learned so much about backend development and architecture with AdonisJS that testing was clearly not a priority. This was the first time I could split features and avoid internal dependencies. Much cleaner and easier to maintain than Feathers.js or NestJS. Yes, I also tried NestJS, but this framework is a real joke to me.
By the end of August 2021, I embarked on a new side project called Insamee, a student ecosystem. That was the successor of the tutoring platform. The architecture is important to clarify. Initially, it was an API server with AdonisJS and four front-end apps using Nuxt. Overkill and overly complicated, as I realize now.
By the end of 2021, I rewrote the project from scratch using only Adonis and a single server. No more tests.
I managed to pull myself away from school classes to focus on my project. A real opportunity. To validate my semester, I had to write a report and make a live presentation. During the presentation, I didn't talk about testing (there were none). However, the jury questioned me about it. I just said the runner was not ready yet, so I hadn't written any tests. They took a few minutes to explain that testing is crucial, and I clearly understood that a real and professional project must have tests—it's non-negotiable. In my mind, something clicked.
Subsequently, from April to August 2022, I did a four-month internship at a consulting company in Paris. I was responsible for building, from scratch, an administration panel for a high-performance computing cluster project using Angular 14. This was a first for me. I had never used Angular before. It took me months to grasp the concept of TestBed
and the dependency injection (DI) system. I was so lost, but later, I discovered that dependency injection is important. During that period, I learned concepts but had no real practice.
In 2024, I did my six-month internship at a Swiss cloud company. I was part of a team developing components for other projects. This is where I truly began to appreciate testing. Firstly, the coverage was at 90%. This means if the coverage drops below 90%, you have no other choice than to write tests to submit an MR. The project was eight months old, so there were plenty of tests to reference. I also wrote many end-to-end tests using Playwright.
Simultaneously, I watched a live stream by Romain Lanz discussing "writing testable code" and explaining the DI in Adonis. With these explanations and the concrete examples I encountered during my internship, things started to come together and make sense.
This year, at Devoxx 2024, I tried to attend as many talks about TDD and DDD as possible. Hearing experts discuss real-world examples and how they architect their apps to make them testable and future-proof was truly enlightening.
Speaking at Devoxx France 2024: Insights and HighlightsDespite all this, I still struggle to find genuine and practical examples of testing. It's so frustrating to know the theory but not be able to apply it.
But I believe that to learn, there is nothing more effective than taking action. So I took the framework I know best, Nuxt, and started experimenting with the available testing tools. I used the end-to-end integration with Playwright, but it was not a pleasant experience. Tests took so long to run. Strangely, the experience during my internship felt the opposite. I discovered that the Nuxt integration rebuilds the application before every test. I made a PR to allow the runner to reuse an existing server.
Simultaneously, I received feedback about my fork of the Plausible tracker. Some bugs were reported, and new features were requested. The problem was the project had no tests, and I clearly remembered how painful it was on the tutoring platform. I also noticed I hadn't touched the project for a while, and manual testing wouldn't be enough to ensure the quality of the project in the long term. When you haven't touched a project for some time, you have to relearn everything. With reliable tests, you can be confident that the project works as expected, even after refactoring, bug fixes, and new features. I decided to write tests before touching anything else. It took me three days, but now, I'm confident that the project works as expected. This is satisfying, and there is no more fear of touching the project.
Finally, around the same time, I discovered Laracasts. I learned PHP and encountered testing in Laravel. It was eye-opening. The testing is well explained, writing tests, even with a database, is easy, and it runs so fast. Writing tests with Pest is truly enjoyable. If you have never tried it, you should.
Try LaracatsUnderstanding the Value of Testing
It took me six years to grasp the value of testing and write meaningful tests. Is that long? I'm not sure. It demonstrates that learning is an iterative process, with failures and retries. With perseverance, you finally get there.
Throughout this journey, I asked myself many questions. Here are some answers I would have loved to find earlier.
- Testing does not have to be difficult.
- However, testing is more challenging than
expect(1 + 1).toBe(2)
. - The difficulty of testing often relates to the application's architecture and the ability to separate features. The more modular the app is, the easier it is to test.
- Before choosing a tool, consider the ability to test your app. If you can't test it, it's not worth it.
- No tests are acceptable. It's a trade-off. Use it wisely.
- Rule 5 does not invalidate rule 4 or rule 3. If you end up writing tests, you will thank yourself.
- Aiming for 100% coverage is meaningless by itself. It's a trap that can lead to poor practices.
- Both unit, integration, and end-to-end tests are valuable. Choose the one that best fits your needs.
- Structure your test with the
arrange
,act
, andassert
method. It's a good method for organizing your test and starting to write them. - Write the
assert
part first. - Think of a test as a description of a specification of the application. My app should display/do this when this happens.
- And more importantly, writing testable code before writing tests. This approach will make your code more readable, maintainable, and testable.
Finally, do not attempt to test everything from day one in this journey. Start small, learn, write simple unit tests, understand how to modularize your code, and progress from there. It's a long journey, but it's worth it.
I'm still learning a lot, but I'm confident that I'm on the right path. See you in the next article!
Laracasts is the best resource I've found for learning testing. PHP is great, and learning general concepts is even better. Try it now.