01/02/2023
10 min read
React

Content:

Dependency injection with React Context, DI VS Mocking Frameworks




Introduction


React Context is often misunderstood, a lot of people think that it is a state manager but it is not.

React Context is a dependency injection mechanism, a portal that allow us to to pass props to all the components wrapped

into a Context without prop drilling. React Contex + useState or useReducer is a state manager but in my opinion not a good

solution for a serious app because it will cause you performance problems if your states change often. I usually use Redux tool kit

or Zustand as a state manager for my React apps and they both use Context under the hood so the pattern that i will show

you in this article or also applicable with any state manger that use Context. We will also see the advantage of using dependency

injection (DI) instead of mocking framework.



Integration Testing


In our app React app a common use case is to make API calls to retrieve, post data etc…

Let's implement a todos service that fetch a list of todos from a dummy API, we want to verify that this service hit the good url,

so we want to use the real service in our test, that is called integration testing. i usually use MSW or Nock for that kind of testing

in this exemple i'll use MSW and during all the exemples of this article i'll use Vitest as my testing framework, Vitest has the same

API than Jest so it will look familiar even if you didn't use it yet. MSW use an express server under the hood and will intercept

the request to the dummy todos API and give us back some fixtures that i have configured myself.


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 import { rest } from "msw"; import { setupServer } from "msw/node"; import { expect, it, beforeAll, afterAll, afterEach } from "vitest"; import { todosService } from "../todos-service"; import { fetch } from "cross-fetch"; const todos = [ { userId: 1, id: 1, title: "delectus aut autem", completed: false, }, { userId: 1, id: 2, title: "quis ut nam facilis et officia qui", completed: false, }, ]; const server = setupServer( rest.get("https://jsonplaceholder.typicode.com/todos", (req, res, ctx) => { return res(ctx.json(todos)); }) ); global.fetch = fetch; beforeAll(() => { server.listen({ onUnhandledRequest: `error` }); }); afterEach(() => { server.resetHandlers(); }); afterAll(() => { server.close(); }); it("should get a list of todos", async () => { const result = await todosService.get(); expect(result).toEqual(todos); });


There is a bit of setup it is often the case with integration tests, now we will implement the service to make this test pass.


1 2 3 4 5 6 7 8 9 import { TodosService } from "./todo"; export const todosService: TodosService = { get: async () => { const res = await fetch("https://jsonplaceholder.typicode.com/todos"); const data = await res.json(); return data; }, };


In the real world we would have also handle the case where the service fail but it is not the point of this article. Why not setup

MSW globally to intecept all our API calls in our test ? Because that kind of test are pretty slow 78 ms for this one , in a UI test it

would have been between 100 ms and 300 ms and in a serious app where you have a thousand of tests all that time start to

quickly add up and increase your feed back loop. That kind of testing is important to ensure that your app work but we should

limit it to the infrastructure level of our app. What to to instead when we are testing our buisness logic or our components ?

Just use a fake service and inject it. for the buisness logic it is easy we can inject our fake service in the params of our

functions or class but how do we inject dependency in our React components ?


Testing with Dependency injection


We will see how to inject dependency with Context through a UI test that use the same service than earlier. We are testing that

we diplay the titles of the list of todos that we get from the dummy api. we will use a fake service instead of the real dependency.


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import { describe, it, expect } from "vitest"; import { render, waitFor, screen } from "@testing-library/react"; import { buildInMemoryTodosService } from "../../todos/in-memory-todos-service"; import TodosServiceProvider from "../TodosServiceProvider"; import Todos from "../Todos"; describe("Todos", () => { it("should retrieve a list of todos and display their titles", async () => { render( <TodosServiceProvider todosService={buildInMemoryTodosService()}> <Todos /> </TodosServiceProvider> ); await waitFor(() => { expect(screen.getByText("delectus aut autem")).toBeInTheDocument(); }); expect( screen.getByText("quis ut nam facilis et officia qui") ).toBeInTheDocument(); }); });


1 2 3 4 5 6 7 8 import { Todo, TodosService } from "./todo"; import { todos } from "./todos-fixture"; export const buildInMemoryTodosService = ( // The fake service data: Todo[] = todos ): TodosService => ({ get: () => Promise.resolve(data), });


To make the test pass we need to implement these two components.


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import { TodosService } from "../todos/todo"; import { createContext, FC, ReactNode } from "react"; import { todosService } from "../todos/todos-service"; export const TodosServiceContext = createContext(todosService); type TodosServiceProps = { todosService: TodosService; children: ReactNode; }; const TodosServiceProvider: FC<TodosServiceProps> = ({ todosService, children, }) => ( <TodosServiceContext.Provider value={todosService}> {children} </TodosServiceContext.Provider> ); export default TodosServiceProvider;


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import { useEffect, useState, useContext } from "react"; import { Todo } from "../todos/todo"; import { TodosServiceContext } from "./TodosServiceProvider"; const Todos = () => { const todosService = useContext(TodosServiceContext); const [todos, setTodos] = useState<Todo[]>([]); useEffect(() => { const getTodos = async () => { const data = await todosService.get(); setTodos(data); }; getTodos(); }, []); return ( <div> {todos.map(({ title, id }) => ( <span key={id}>{title}</span> ))} </div> ); }; export default Todos;


Quite simple isn't it ? Well it is a bit more setup than if we don't use DI but this test is quite fast for a UI test ( 30ms) , our app is

more decoupled and we can use this fake service in production if we don't have a real one yet, so we can code our frontend

even if the backend is not ready and make demos.

With this pattern we can swap all our dependencies and setup quickly a little script to run our app with our fake dependencies

instead of the real ones and it is very useful when the backend is down. Lastly you don't need to remember any additional

syntax , your test will look 99% the same with whatever testing framework that you use.


Testing With Mocking Framework


I'll now provide you the exact same code than above but without DI.


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import { describe, it, afterEach, expect, vi } from "vitest"; import { render, screen, waitFor } from "@testing-library/react"; import Todos from "../Todos"; import { todos } from "../../todos/todos-fixture"; import { todosService } from "../../todos/todos-service"; describe("Todos", () => { afterEach(() => { vi.restoreAllMocks(); }); it("should retrieve a list of todos and display their titles", async () => { vi.spyOn(todosService, "get").mockResolvedValue(todos); render(<Todos />); await waitFor(() => { expect(screen.getByText("delectus aut autem")).toBeInTheDocument(); }); expect( screen.getByText("quis ut nam facilis et officia qui") ).toBeInTheDocument(); }); });



1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import { useEffect, useState } from "react"; import { Todo } from "../todos/todo"; import { todosService } from "../todos/todos-service"; const Todos = () => { const [todos, setTodos] = useState<Todo[]>([]); useEffect(() => { const getTodos = async () => { const data = await todosService.get(); setTodos(data); }; getTodos(); }, []); return ( <div> {todos.map(({ title, id }) => ( <span key={id}>{title}</span> ))} </div> ); }; export default Todos;


There is obviously a bit less setup with this version and the test is also fast (37ms) but there are some drawbacks.

You are mocking the real dependency , so if you make a mistake while mocking your code (and it is easy to do so when you are not experienced)

you will call the real dependency and potentially insert data in your database for exemple.

If you're using you dependency in a lot of tests you'll have to mock your dependency in each test. You're mocks live only in your test code,

so you can't use it in production while the backend is not ready.You'll also need to learn how to mock with your testing framework and

the syntax is often pretty weird (it is just my opinon) , so if you switch of testing framework you'll need to relearn how to mock with your

new testing framework. More importanly with mocking in this way you can still test your code even if it is tighly coupled to your dependencies,

it may seems easier but it is a nightmare to maintain because this coupling will limits your refactoring moves.


Sum up


Dependency injection advantages:






Dependency injection drawbacks:




The code snippets used in this article are available in this repo.