//
Busca en todas las páginas de la documentación
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Prueba stores de Zustand restableciendo el state entre tests, usando getState() y setState() para aserciones directas, y simulando stores en tests de componentes.
// stores/counter-store.ts
import { create } from "zustand";
interface CounterStore {
count: number;
increment: () => void;
decrement: () => void;
reset: () => void;
}
export const useCounterStore = create<CounterStore>((set) => ({
count: 0,
increment: () => set((s) => ({ count: s.count + 1 })),
decrement: () => set((s) => ({ count: s.count - 1 })),
reset: () => set({ count: 0 }),
}));// __tests__/counter-store.test.ts
import { useCounterStore } from "@/stores/counter-store";
// Restablecer el store antes de cada test
beforeEach(() => {
useCounterStore.setState({ count: 0 });
});
describe("CounterStore", () => {
it("starts at zero", () => {
expect(useCounterStore.getState().count).toBe(0);
});
it("increments", () => {
useCounterStore.getState().increment();
expect(useCounterStore.getState().count).toBe(1);
});
it("decrements", () => {
useCounterStore.setState({ count: 5 });
useCounterStore.getState().decrement();
expect(useCounterStore.getState().count).toBe(4);
});
it("resets to zero", () => {
useCounterStore.setState({ count: 42 });
useCounterStore.getState().reset();
expect(useCounterStore.getState().count).toBe(0);
});
});// stores/todo-store.ts
import { create } from "zustand";
interface Todo {
id: string;
text: string;
done: boolean;
}
interface TodoStore {
todos: Todo[];
addTodo: (text: string) => void;
toggleTodo: (id: string) => void;
removeTodo: (id: string) => void;
getActiveTodos: () => Todo[];
getCompletedCount: () => number;
}
export const useTodoStore = create<TodoStore>((set, get) => ({
todos: [],
addTodo: (text) =>
set((s) => ({
todos: [...s.todos, { id: crypto.randomUUID(), text, done: false }],
})),
toggleTodo: (id) =>
set((s) => ({
todos: s.todos.map((t) => (t.id === id ? { ...t, done: !t.done } : t)),
})),
removeTodo: (id) =>
set((s) => ({ todos: s.todos.filter((t) => t.id !== id) })),
getActiveTodos: () => get().todos.filter((t) => !t.done),
getCompletedCount: () => get().todos.filter((t) => t.done).length,
}));// __tests__/todo-store.test.ts
import { useTodoStore } from "@/stores/todo-store";
import { act } from "@testing-library/react";
const initialState = useTodoStore.getState();
beforeEach(() => {
useTodoStore.setState(initialState, true); // true = replace
});
describe("TodoStore", () => {
it("adds a todo", () => {
act(() => {
useTodoStore.getState().addTodo("Comprar leche");
});
const { todos } = useTodoStore.getState();
expect(todos).toHaveLength(1);
expect(todos[0].text).toBe("Comprar leche");
expect(todos[0].done).toBe(false);
});
it("toggles a todo", () => {
// Precargar el state
useTodoStore.setState({
todos: [{ id: "1", text: "Prueba", done: false }],
});
act(() => {
useTodoStore.getState().toggleTodo("1");
});
expect(useTodoStore.getState().todos[0].done).toBe(true);
});
it("removes a todo", () => {
useTodoStore.setState({
todos: [
{ id: "1", text: "A", done: false },
{ id: "2", text: "B", done: true },
],
});
act(() => {
useTodoStore.getState().removeTodo("1");
});
expect(useTodoStore.getState().todos).toHaveLength(1);
expect(useTodoStore.getState().todos[0].id).toBe("2");
});
it("computes active todos", () => {
useTodoStore.setState({
todos: [
{ id: "1", text: "A", done: false },
{ id: "2", text: "B", done: true },
{ id: "3", text: "C", done: false },
],
});
expect(useTodoStore.getState().getActiveTodos()).toHaveLength(2);
expect(useTodoStore.getState().getCompletedCount()).toBe(1);
});
});// __tests__/todo-component.test.tsx
import { render, screen, fireEvent } from "@testing-library/react";
import { useTodoStore } from "@/stores/todo-store";
import { TodoList } from "@/components/todo-list";
// Restablecer antes de cada test
beforeEach(() => {
useTodoStore.setState({ todos: [] }, true);
});
describe("TodoList component", () => {
it("renders todos from the store", () => {
useTodoStore.setState({
todos: [
{ id: "1", text: "Escribir tests", done: false },
{ id: "2", text: "Publicar funcionalidad", done: true },
],
});
render(<TodoList />);
expect(screen.getByText("Escribir tests")).toBeInTheDocument();
expect(screen.getByText("Publicar funcionalidad")).toBeInTheDocument();
});
it("adds a todo via the form", async () => {
render(<TodoList />);
const input = screen.getByRole("textbox");
const button = screen.getByRole("button", { name: /add/i });
fireEvent.change(input, { target: { value: "Nuevo todo" } });
fireEvent.click(button);
expect(useTodoStore.getState().todos).toHaveLength(1);
expect(useTodoStore.getState().todos[0].text).toBe("Nuevo todo");
});
});getState() y setState() en el propio hook, lo que permite inspeccionar y manipular el state directamente sin renderizar componentes.setState(state, replace) con replace: true reemplaza todo el state en lugar de fusionarlo. Esto es esencial para restablecer el state limpiamente en los tests.getState(). Llámalas directamente en los tests.setState antes de renderizar.Utilidad global de restablecimiento:
// test-utils/reset-stores.ts
import { useCounterStore } from "@/stores/counter-store";
import { useTodoStore } from "@/stores/todo-store";
const stores = [
{ store: useCounterStore, initial: { count: 0 } },
{ store: useTodoStore, initial: { todos: [] } },
];
export function resetAllStores() {
stores.forEach(({ store, initial }) => {
(store as any).setState(initial, true);
});
}
// En setupTests.ts
beforeEach(() => resetAllStores());Simular un store por completo:
// __tests__/header.test.tsx
import { vi } from "vitest";
vi.mock("@/stores/auth-store", () => ({
useAuthStore: vi.fn((selector) =>
selector({
user: { name: "Test User", role: "admin" },
token: "fake-token",
isAuthenticated: () => true,
logout: vi.fn(),
})
),
}));
import { render, screen } from "@testing-library/react";
import { Header } from "@/components/header";
it("shows admin panel link for admin users", () => {
render(<Header />);
expect(screen.getByText("Panel de administración")).toBeInTheDocument();
});Probar acciones async:
import { useTodoStore } from "@/stores/todo-store";
// Simular fetch
global.fetch = vi.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve([{ id: "1", text: "Desde la API", done: false }]),
} as Response)
);
it("fetches todos from API", async () => {
await useTodoStore.getState().fetchTodos();
expect(fetch).toHaveBeenCalledWith("/api/todos");
expect(useTodoStore.getState().todos).toHaveLength(1);
expect(useTodoStore.getState().todos[0].text).toBe("Desde la API");
});Probar suscripciones:
it("calls subscriber on state change", () => {
const listener = vi.fn();
const unsub = useCounterStore.subscribe(listener);
useCounterStore.getState().increment();
expect(listener).toHaveBeenCalledTimes(1);
expect(listener).toHaveBeenCalledWith(
expect.objectContaining({ count: 1 }),
expect.objectContaining({ count: 0 })
);
unsub();
});getState() devuelve el state tipado completo, incluidas las acciones.setState acepta Partial<State> o (s: State) => Partial<State>.// Mock con tipado seguro
const mockState: ReturnType<typeof useAuthStore.getState> = {
user: { name: "Test", email: "test@test.com", role: "admin" },
token: "fake",
login: vi.fn(),
logout: vi.fn(),
isAuthenticated: () => true,
};setState({}, true) con replace: true elimina todas las propiedades, incluidas las acciones. Pasa el state inicial completo, incluidas las referencias a las acciones, o simplemente restablece las propiedades de datos.vi.mock es a nivel de módulo y se aplica a todos los tests del archivo. Usa vi.fn().mockReturnValue() por test para distintos states simulados.act() de @testing-library/react cuando los cambios del store provocan actualizaciones de state de React en componentes renderizados.await en los tests. Usa await store.getState().asyncAction() o waitFor de testing-library.persist, los tests pueden intentar acceder a localStorage. Simúlalo o usa un adaptador de almacenamiento en memoria en los tests.| Enfoque | Ventajas | Desventajas |
|---|---|---|
| getState/setState directo | No requiere renderizado, tests unitarios rápidos | No prueba la integración con componentes |
| Tests de componentes con store real | Prueba la integración completa | Más lento, más configuración |
| Store simulado (vi.mock) | Aísla el componente de la lógica del store | El mock puede divergir del store real |
| createStore por test | Aislamiento total, sin necesidad de restablecer | Más código repetitivo |
beforeEach(() => {
useCounterStore.setState({ count: 0 });
});setState con el state inicial antes de cada test.setState(initialState, true) con replace: true para un restablecimiento completo.it("increments", () => {
useCounterStore.getState().increment();
expect(useCounterStore.getState().count).toBe(1);
});getState() para llamar a las acciones y leer el state directamente.setState antes de render() para precargar el store con datos de test.vi.mock("@/stores/auth-store", () => ({
useAuthStore: vi.fn((selector) =>
selector({
user: { name: "Test User" },
logout: vi.fn(),
})
),
}));vi.mock a nivel de módulo. El mock se aplica a todos los tests del archivo.replace: true.replace para mantener las acciones intactas.act() de @testing-library/react.act() no siempre es necesario.it("fetches todos", async () => {
global.fetch = vi.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve([{ id: "1", text: "Test" }]),
} as Response)
);
await useTodoStore.getState().fetchTodos();
expect(useTodoStore.getState().todos).toHaveLength(1);
});fetch y luego usa await en la acción async directamente.it("calls subscriber on change", () => {
const listener = vi.fn();
const unsub = useCounterStore.subscribe(listener);
useCounterStore.getState().increment();
expect(listener).toHaveBeenCalledTimes(1);
unsub();
});const mockState: ReturnType<typeof useAuthStore.getState> = {
user: { name: "Test", email: "t@t.com", role: "admin" },
token: "fake",
login: vi.fn(),
logout: vi.fn(),
isAuthenticated: () => true,
};ReturnType<typeof store.getState> para un mock con tipado seguro.Revisado por Chris St. John·Última actualización: 7 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥