How to design testing for a project?
After deciding to adopt Vitest into the project, and before building out the test environment, I found myself taking another look at the project and thinking about how best to structure the test code overall.
Here, by how, I mean going beyond simply adding test code—I wanted the test code itself to be structured in a way that could have a positive impact on the entire project's structure. By positive impact, I mean that it should not only let me check whether the structure makes test code easy to write, but also let me verify, as I add test code, whether the project's code is good (?) code. With those considerations in mind, I looked for design patterns that might help with adopting testing, and I want to briefly summarize the design patterns I found and the test-adoption structure I plan to pursue.

Design Patterns for Testing
Dependency Injection
The Dependency Injection pattern is one you've probably heard of and are familiar with. It's a design approach where, instead of creating objects or logic directly inside, you inject them from the outside, thereby improving reusability and extensibility.
As shown below, we create a hook that manages the name state, and have the logic that fetches user information via the API be injected from the outside.
tsxexport const useUserName = (fetchUser: () => Promise<string>) => {
const [name, setName] = useState("");
useEffect(() => {
fetchUser().then(setName);
}, [fetchUser]);
return name;
};
For components as well, we design them so that the state-query API needed for actual rendering is injected from the outside through props.
tsxtype UserProps = {
fetchUser: () => Promise<string>;
};
export const User = ({ fetchUser }: UserProps) => {
const name = useUserName(fetchUser);
return <div>name</div>;
};
By designing functions or objects such as APIs to be injected from the outside as shown above, we make it possible, at the testing stage, to replace business logic and external dependencies with mocks so that they can be tested independently.
tsximport { renderHook, act } from "@testing-library/react";
import { useUserName } from "./useUserName";
test("fetches and returns user name", async () => {
const mockFetchUser = jest.fn().mockResolvedValue("Luffy");
const { result } = renderHook(() => useUserName(mockFetchUser));
// wait for effect
await act(async () => {});
expect(mockFetchUser).toHaveBeenCalled();
expect(result.current).toBe("Luffy");
});
Widely known patterns such as the Strategy Pattern and the Repository Pattern are design patterns extended from the dependency injection pattern, so I judged their core parts to be similar and will omit those summaries.
State Reducer Pattern
The State Reducer Pattern is one of the design patterns frequently mentioned when designing React. By separating state-management logic into a pure function (reducer), you can design a structure decoupled from the component, which makes it a useful pattern for state management and test design.
As shown below, we create a reducer that handles state and actions, and then use it in the state-management logic.
tsx// reducer.ts
export type CounterState = { count: number };
export type CounterAction = { type: "increment" } | { type: "decrement" };
export function counterReducer(
state: CounterState,
action: CounterAction,
): CounterState {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
default:
return state;
}
}
The current project uses Zustand as its state-management library, and below is a simple example of combining the reducer defined above with Zustand.
tsx// userCounterStore.ts
import { create } from "zustand";
import { counterReducer, CounterState, CounterAction } from "./reducer";
type Store = CounterState & {
dispatch: (action: CounterAction) => void;
};
export const useCounterStore = create<Store>((set) => ({
count: 0,
dispatch: (action) => set((state) => counterReducer(state, action)),
}));
By distributing the state-handling logic and management points as shown above, actual tests can verify only the state-handling logic that has been separated into a pure function, allowing for clearer unit tests.
tsximport { counterReducer } from "./reducer";
expect(counterReducer({ count: 0 }, { type: "increment" })).toEqual({
count: 1,
});
Humble Object Pattern
Humble means simple: you make the hard-to-test parts simple, and separate the parts that need testing into distinct logic.
Let's convert the hook we used in dependency injection into the humble object pattern. We separate the logic that needs API testing into a distinct function, and have the hook use that function. The API request handling, which corresponds to important business logic, is separated into a distinct function and tested, while the hook and component simply perform the role of humble objects.
tsxexport async function fetchUser(): Promise<string> {
const response = await fetch("/api/user");
const data = await response.json();
return data.name;
}
export const useUserName = () => {
const [name, setName] = useState("");
useEffect(() => {
fetchUser().then(setName);
}, [fetchUser]);
return name;
};
By handling the API request part processed in fetchUser with jest.Mock, we can make its result simple and independently testable.
jsxtest('fetchUser returns user name from API', async () => {
const mockData = { name: 'Alice' };
(fetch as jest.Mock).mockResolvedValueOnce({
json: () => Promise.resolve(mockData),
});
const result = await fetchUser();
expect(fetch).toHaveBeenCalledWith('/api/user');
expect(result).toBe('Alice');
});
It would be ideal to test useUserName itself as a hook, but because its actual logic depends on the library's useState and useEffect hooks, testing it requires additional mocking and asynchronous control (act, waitFor), just as we saw with dependency injection.
Which design pattern should we use?
Besides the patterns summarized above, there are probably countless design patterns that help with adopting testing. Many people may already know this, but as you look through the design patterns above, you'll notice they share a common trait. That is, they clearly separate the logic considered to need testing—or important logic—from the logic that does not.
Starting from a large scale, this commonality can apply not only to projects, libraries, and the elements we commonly call modules, but even further down to the level of a function that performs a single role. When you separate important logic into units that can each perform a single independent role like this, the tests also become a structure that can be run as separate, independent tests. So then, what kind of structure or design pattern is best to separate them with?
In conclusion
My thoughts on the question, "What structure should I design with, or which design pattern is best to use?" came together to some degree as I researched design patterns and recently implemented some parser logic.
During a review of the initial draft of the parser logic, there was a time when we exchanged questions and answers about "why did you use that design pattern?" Through this process of refactoring the implemented logic, I came to think it would be good to proceed in this same direction for the project where I'll be adopting tests. Until now, whenever I laid out or designed a project structure, I think I always considered "which design pattern should I use?" only from the standpoint of the largest unit—the project. But this time, through researching design patterns and implementing the parser logic, I felt that from a single large module all the way down to the function level, there are various design patterns suited to each one's role, and that applying them appropriately to the situation is more desirable.

Thoughts like the above may not be the right answer either, and the process of considering the design pattern suited to a given piece of logic every time you write a test will likely not be easy. There already exist many great articles and materials on testing, but I think the deliberation over testing—which changes depending on each person's environment—will end up being an endless battle.