Dependency injection allows for a decoupled design and makes testing easier. It fosters clear boundaries between components and allows for the simple substitution of one concrete implementation of a dependency with another concrete implementation. It also allows for substitution of a dependency with a mock, or fake, during testing.
This blog details some techniques for using dependency injection in React.
Where Dependency Injection Takes Place in React
In essence, dependency injection in React occurs through a component's props and children, which are the public-facing seams of a component.
Props define the specific dependencies that a component will reference and utilize within its internals. Typically, a direct or indirect parent (through a parent chain) will define and set the concrete implementations for these props. It is possible to default a prop in the child and not pass anything through the parent; however, this still means that these props are defined at the public-facing interface, rather than hidden internally within the component itself.
As demonstrated in the examples below, a component's props are arbitrary inputs that can consist of anything—including another component, a function, or an object. A component can also utilize a special "children" prop to render children components. This is a powerful tool based on the principle of composition, and it allows a component's parent to swap the component's own children without needing to change the internals of either components.
Let’s talk about how this makes testing easier using an example application. Say we have a web application for a pie company. In our app, we have a form that users can fill to order a pie.
In our code, we are looking to build a component called OrderPie that connects to the data store and displays a user’s name on the page. We also want another component called PieForm that displays the form and allows the user to submit the order details. Assume the form has various validations to ensure that the user provides all the required information.
Note, there is some pseudo code in the examples to hide-away the extraneous implementation details relating to the data store, routing, and endpoint calls. This will allow us to focus on the structure of the React components themselves. Also note that the tests below use methods from the Jasmine and Enzyme testing libraries.
Example 1: Decoupling the Form by Injecting it Through Props
Now let’s say we are test-driving the OrderPie component to ensure that the user is redirected to an order preview page upon form submission. This test is not concerned with what the user ordered or whether the user entered all the required data. It only wants to make sure the user is sent to the right page upon submitting the form.
Having the form get passed into the component as a prop allows us to define a collaborator that makes this test relatively easy to set up, and makes the test stay focused on its true purpose.
Note that the FakeForm and PieForm components are polymorphic. They contain the same number of props, with the same prop names, each of which pass in the same data type. This allows for simple substitution and doesn't break any functionality. We can use a real form in the production code and a fake form in our test code.
This also allows us to unit test this feature without having an expensive integration test that has to deal with the real form and any potential validations.
Example 2: Decoupling the Endpoint Action by Injecting a Function to Perform an Action
Let’s say we are going to test drive the OrderPie component’s ability to call an endpoint to POST /Pie to submit the pie order. This test is not concerned with the actual endpoint call itself, just that it is made when the form is submitted.
If we were to directly call this endpoint within the OrderPie component itself, it would make it part of the internal workings of the component. It would be difficult and messy to set up a spy to make sure the endpoint is called for the same reason that private functions cannot be tested and concrete implementations within a class cannot be substituted. So instead, we delegate the responsibility for the endpoint call to a function,
orderPieAction. This function then becomes a dependency, injected into the
<OrderPie/> component through the
orderAction prop. We can then easily replace this dependency with a simple spy when testing what
<OrderPie/> does when the submit button is clicked on, as demonstrated below.
When we invert the control and pass this action through the props, we can now spy on it and make sure it is called when it should be. This prevents us from making a real endpoint call upon form submission in the test.
Example 3: Decoupling the Rendering of Components with Children
In one last example, let’s say we are going to test drive the app’s ability to prevent a user from ordering a pie when there is a billing delinquency. Instead of displaying the pie order form, the app will display the pending charges page.
To adhere with composition and the single responsibility principle, we are going to build a component called
<CheckForOutstandingCharges/>, whose sole responsibility is to get the user's billing history and decide whether to redirect to the pending charges page or not. Our test is not concerned with exactly what components get displayed when there is no delinquency, just that they do. To allow the parent of
<CheckForOutstandingCharges/> to inject the components it wants to render when there is no delinquency and to have the test render a simple fake component, we will have
<CheckForOutstandingCharges/> invoke a special prop,
children, which refers to any JSX elements that the parent nests within itself.
As a reminder, we want to be able to render the actual OrderPie component in the production code and a simple fake component in our unit test.
By using the
children prop, we can isolate our scope to ensure the behaviors of only this component during testing. We can check that its children are rendered when there is no delinquency and the user is redirected to the expected page when there is. The structure of the App, and what the children are exactly, is not the test's concern.
The bonus for this type of design is that you can prevent having to integration-test the features. Due to the well-defined, public-facing seams in your components, you can leverage decoupled, granular, faster, and easier unit tests instead.