This article is based on Laurent Cazanove's talk at Vue.js Nation 2025, titled "How dependency injection rescued my app from the untestable abyss." In this session, Laurent discussed the challenges of leaking implementation details, bloated composables, and untestable code—a situation many developers find themselves in. What began as a "keep it simple" approach turned into a complex web of business logic, API calls, and retry mechanisms.
In Vue.js Nation 2025 Laurent shared insights on utilizing Nuxt plugins to implement dependency injection as a strategy to clean up architecture, decouple logic, and simplify testing processes without falling into the trap of over-engineering.
Laurent shared his experience with a legacy app running on Firebase, which he wanted to migrate to Superbase. Despite having good integration tests, he found that his business logic was too tightly coupled with Firebase code. This coupling led to leaking implementation details and bloated composable code.
The tightly coupled codebase made the application difficult to test and maintain. It was challenging to identify a starting point for migration.
"I had good integrations test using Firebase, but, my business logic was just, like, too tightly coupled with the Firebase code. […] It was really hard to know where to start for this migration, and what felt particularly bad was that there was no way for me to leverage my tests to help in that migration." ~ Laurent
In his talk, Laurent shared a key solution: dependency injection. Dependency injection helps in building testable apps by cleaning up composables, decoupling business logic from dependencies, and writing more testable code.
Key Learnings
UserList
ComponentLaurent introduces a simple example of a UserList
component.
The component utilizes a getUsers
function provided by the useUsers
composable. This function is employed within an asynchronous data hook to handle data fetching efficiently.
Using Supabase Client:
The useUsers
composable relies on the Supabase client for its operations:
getUsers
function retrieves users from the database using Supabase.This example highlights how dependency injection can streamline your code by allowing components to easily access shared functionality without tightly coupling them to specific implementations.
Testing a component in Nuxt involves several steps. First, import the necessary modules. Then, define the test case to check specific functionalities.
One common challenge is deciding what to mock in tests. For instance, instead of using an actual database like Supabase during tests, you might consider using an in-memory database to speed up local testing. However, simply mocking functions like useUsers
might lead to repetitive mock implementations for each test.
The Vue.js documentation advises focusing on inputs and outputs rather than implementation details. This means checking props and interactions as inputs and validating rendered elements or emitted events as outputs.
Mocking implementation details like useUsers
can lead to brittle tests that break with minor changes in function names or logic. Instead of this approach, consider mocking at a higher level by replacing dependencies such as databases with mock versions that return expected data.
To improve readability and maintainability of tests, focus on dependencies rather than business logic. Mocking database clients instead of specific functions can help avoid complex factory implementations or rewriting mocks for every test case.
Dependency Injection (DI) is a design pattern that helps decouple code by injecting dependencies from the outside, rather than having components create their own.
Laurent explains that while mocking the database might work, it often leads to tightly coupled code with dependencies like Supabase. This tight coupling makes it difficult to test and migrate databases because the code is deeply integrated with specific dependencies.
To solve these issues, Laurent suggests introducing an extra level of indirection (what this means is a level of abstraction). This involves wrapping dependencies that are not owned by the application in a controlled API.
useDatabase
) that wraps the third-party dependency (e.g., useSupabaseClient
).findAll
to handle operations internally and abstract database interactions.The speaker points out that while this approach makes code easier to read and test by focusing on business logic rather than dependency details, it still doesn't allow for easy swapping of databases within tests without additional setup.
Laurent concludes implementing Dependency Injection by wrapping third-party dependencies into controlled APIs allows for greater flexibility in testing and migration. While this approach has its limitations regarding swapping databases without additional configurations in tests, it still provides significant benefits in decoupling logic from specific implementations.
Here are the main benefits:
useDatabase
isn't responsible for retrieving or configuring its database.At the end of his talk, Laurent highlighted how leveraging Nuxt plugins for dependency injection can help clean up architecture, decouple logic, and simplify testing without over-engineering.
The session underscored the importance of the separation of concerns in software architecture. By pulling dependencies from the IoC container, business logic remains free from dependency concerns. This approach allows for dependencies to be injected from the outside, improving testability by enabling dependency swapping at test level.
A significant benefit of this methodology is the decoupling of business logic from data retrieval tasks.
If you want to learn more about dependency injection, you can watch Laurent’s talk on YouTube.
Our goal is to be the number one source of Vue.js knowledge for all skill levels. We offer the knowledge of our industry leaders through awesome video courses for a ridiculously low price.
More than 200.000 users have already joined us. You are welcome too!
© All rights reserved. Made with ❤️ by BitterBrains, Inc.