Testing React components is relatively straightforward. However, you might run into an instance where some of the modules imported in your component are getting in the way of testing its functionality, and you'd like a way to work around them.
A technique that can be used to help with this problem is called dependency injection. In this post, I'll cover what dependency injection is and a simple method of implementing it so you can get on testing your components!
What is Dependency Injection?
Dependency injection (or DI for short) is a programming technique that passes dependencies to the modules that need them versus the module creating the dependencies themselves. This allows the higher-level module to use a dependency that can be changed at runtime. When a module uses a passed-in dependency instead of the concrete (imported) version, we refer to this as loose coupling. An advantage of loosely coupled modules is that they are typically easier to test.
Now let's look at the problem it tries to solve.
Applications consist of multiple parts. These parts go by many names, such as modules, objects, classes, components, functions, etc. A developer pairs these parts together into a whole to form a working piece of software. For instance, a React developer might be building a component, but that component requires methods, components, or providers imported from other modules. In this instance, we say that those modules are coupled to the component that consumes them.
In a typical JavaScript application, we import the modules we need and use them directly in our application code. When done this way, these modules are considered to be tightly coupled. Tight coupling refers to the fact that one piece of functionality uses the concrete implementation of another piece. Tight coupling components often make components harder to test and resistant to change.
For instance, take the following React component that uses the useHeroes
hook, which fetches data from a backend service:
import { useHeroes } from '../hooks/heroes.hook';
function HereosListPage() {
const { getHeroes } = useHeroes();
const heroes = getHeroes();
return (
<div>
{heroes.map((hero, i) => (
<p key={i}>{hero.name}</p>
))}
</div>
);
}
Above, we refer to the useHereos
hooks as tightly coupled to the HeroListPage
because the component directly uses the hook. Any usage of HeroListPage
will also invoke the code in useHeroes
. Typically, this isn't necessarily a problem, and it's how 99% of all React components are written*.
*50% of all stats are made up.
However, this does provide an issue if we want to test the component without directly using the code in the hook. Calling the hook might have consequences we don't want to deal with in our test, such as making API calls, modifying global state objects, persisting storage, analytics, etc.
Some testing frameworks and runners provide a means to mock imported modules and provide fake versions of them in their place. However, these often involve some trickery under the hood and will only work in some instances. Cypress Component Testing currently does not have a way to mock modules and have them work in all the frameworks and development servers it supports. So, we need a way to get around this, and that's where dependency injection comes into play.
Even though DI is not used that much in the React ecosystem, we can certainly use the technique in our apps to make testing them easier. Other frameworks, like Angular, have a great DI system built into its core. And while DI isn't necessarily built into React, we have a great mechanism to utilize DI without much fuss. Let's see how!
Dependency injection in React via props
There are a few techniques to inject dependencies into their consuming components. Classes can obtain dependencies through constructor parameters or public properties, while functions can use parameter injection. Since React components are just functions, we can leverage the former technique and inject dependencies as props.
Below is the HeroesListPage
component which we saw above, but this time we are passing in a useHeroes
prop and using that instead of the directly imported version of useHeroes
:
import { useHeroes as _useHeroes } from './heroes.hook';
export function HeroesListPage({useHeroes = _useHeroes}) {
const { getHeroes } = useHeroes();
const heroes = getHeroes();
return (
<div>
{heroes.map((hero, i) => (
<p key={i}>{hero.name}</p>
))}
</div>
);
}
Above, if the useHeroes
prop is not provided, it defaults to _useHeroes
, which is the imported hook aliased to a different name to signify that it shouldn't be used directly.
By defaulting the value to the real hook, we can use the component as is in the application code but provide a fake instance of the hook in tests.
Below is a Cypress component test that provides a fake instance of useHeroes
to the component:
import { HeroListPage } from './HeroListPage';
describe('HeroListPage', () => {
it('should render heroes', () => {
cy.mount(
<HeroListPage
useHeroes={() => {
return {
getHeroes: cy
.stub()
.as('getHeroesStub')
.returns([{ id: 1, name: 'The Smoker' }]),
};
}}
/>
);
cy.get('div').contains('The Smoker');
cy.get('@getHeroesStub').should('have.been.called');
});
});
The useHeroes
prop is provided with a fake instance of the getHeroes
method. We use a Cypress Stub to make the method return fake data with an alias that we can verify later on.
While this technique may look weird initially, using prop injection is a simple way to help test your components. It has little to no impact on the application code itself and doesn't require any new libraries.
However, it’s important to ask yourself if you are mocking at the right layer. Suppose the module you import only makes HTTP requests. In that case, you might find it easier (and less confusing in the application code) to mock the requests using cy.intercept()
instead of using dependency injection.
Wrapping Up
There are also a few libraries out there that specialize in dependency injection for React. If you are looking for a more sophisticated solution, I encourage you to check them out. One is InversifyJS, an IOC container built for TypeScript with React-specific extensions.
Check out our docs to write your first component test in 5 minutes or less. As always, if this feature is helpful or if you have other ideas or feedback for our team, let us know on Github. To learn more about component testing in Cypress, visit our guide, Testing Your Components with Cypress.