--- description: globs: alwaysApply: false --- # Testing Patterns & Best Practices ## Test File Naming & Environment ### File Extensions - Use `.test.tsx` for React component/hook tests (runs in jsdom environment) - Use `.test.ts` for utility/service tests (runs in Node environment) - The vitest config uses `environmentMatchGlobs` to automatically set jsdom for `.tsx` files ### Test Structure ```typescript // Import the mocked functions first import { useHook } from "@/path/to/hook"; import { serviceFunction } from "@/path/to/service"; import { renderHook, waitFor } from "@testing-library/react"; import { beforeEach, describe, expect, test, vi } from "vitest"; // Mock dependencies vi.mock("@/path/to/hook", () => ({ useHook: vi.fn(), })); describe("ComponentName", () => { beforeEach(() => { vi.clearAllMocks(); // Setup default mocks }); test("descriptive test name", async () => { // Test implementation }); }); ``` ## React Hook Testing ### Context Mocking When testing hooks that use React Context: ```typescript vi.mocked(useResponseFilter).mockReturnValue({ selectedFilter: { filter: [], onlyComplete: false, }, setSelectedFilter: vi.fn(), selectedOptions: { questionOptions: [], questionFilterOptions: [], }, setSelectedOptions: vi.fn(), dateRange: { from: new Date(), to: new Date() }, setDateRange: vi.fn(), resetState: vi.fn(), }); ``` ### Testing Async Hooks - Always use `waitFor` for async operations - Test both loading and completed states - Verify API calls with correct parameters ```typescript test("fetches data on mount", async () => { const { result } = renderHook(() => useHook()); expect(result.current.isLoading).toBe(true); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); expect(result.current.data).toBe(expectedData); expect(vi.mocked(apiCall)).toHaveBeenCalledWith(expectedParams); }); ``` ### Testing Hook Dependencies To test useEffect dependencies, ensure mocks return different values: ```typescript // First render mockGetFormattedFilters.mockReturnValue(mockFilters); // Change dependency and trigger re-render const newMockFilters = { ...mockFilters, finished: true }; mockGetFormattedFilters.mockReturnValue(newMockFilters); rerender(); ``` ## Performance Testing ### Race Condition Testing Test AbortController implementation: ```typescript test("cancels previous request when new request is made", async () => { let resolveFirst: (value: any) => void; let resolveSecond: (value: any) => void; const firstPromise = new Promise((resolve) => { resolveFirst = resolve; }); const secondPromise = new Promise((resolve) => { resolveSecond = resolve; }); vi.mocked(apiCall) .mockReturnValueOnce(firstPromise as any) .mockReturnValueOnce(secondPromise as any); const { result } = renderHook(() => useHook()); // Trigger second request result.current.refetch(); // Resolve in order - first should be cancelled resolveFirst!({ data: 100 }); resolveSecond!({ data: 200 }); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); // Should have result from second request expect(result.current.data).toBe(200); }); ``` ### Cleanup Testing ```typescript test("cleans up on unmount", () => { const abortSpy = vi.spyOn(AbortController.prototype, "abort"); const { unmount } = renderHook(() => useHook()); unmount(); expect(abortSpy).toHaveBeenCalled(); abortSpy.mockRestore(); }); ``` ## Error Handling Testing ### API Error Testing ```typescript test("handles API errors gracefully", async () => { const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {}); vi.mocked(apiCall).mockRejectedValue(new Error("API Error")); const { result } = renderHook(() => useHook()); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); expect(consoleSpy).toHaveBeenCalledWith("Error message:", expect.any(Error)); expect(result.current.data).toBe(fallbackValue); consoleSpy.mockRestore(); }); ``` ### Cancelled Request Testing ```typescript test("does not update state for cancelled requests", async () => { const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {}); let rejectFirst: (error: any) => void; const firstPromise = new Promise((_, reject) => { rejectFirst = reject; }); vi.mocked(apiCall) .mockReturnValueOnce(firstPromise as any) .mockResolvedValueOnce({ data: 42 }); const { result } = renderHook(() => useHook()); result.current.refetch(); const abortError = new Error("Request cancelled"); rejectFirst!(abortError); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); // Should not log error for cancelled request expect(consoleSpy).not.toHaveBeenCalled(); consoleSpy.mockRestore(); }); ``` ## Type Safety in Tests ### Mock Type Assertions Use type assertions for edge cases: ```typescript vi.mocked(apiCall).mockResolvedValue({ data: null as any, // For testing null handling }); vi.mocked(apiCall).mockResolvedValue({ data: undefined as any, // For testing undefined handling }); ``` ### Proper Mock Typing Ensure mocks match the actual interface: ```typescript const mockSurvey: TSurvey = { id: "survey-123", name: "Test Survey", // ... other required properties } as unknown as TSurvey; // Use when partial mocking is needed ``` ## Common Test Patterns ### Testing State Changes ```typescript test("updates state correctly", async () => { const { result } = renderHook(() => useHook()); // Initial state expect(result.current.value).toBe(initialValue); // Trigger change result.current.updateValue(newValue); // Verify change expect(result.current.value).toBe(newValue); }); ``` ### Testing Multiple Scenarios ```typescript test("handles different modes", async () => { // Test regular mode vi.mocked(useParams).mockReturnValue({ surveyId: "123" }); const { rerender } = renderHook(() => useHook()); await waitFor(() => { expect(vi.mocked(regularApi)).toHaveBeenCalled(); }); // Test sharing mode vi.mocked(useParams).mockReturnValue({ surveyId: "123", sharingKey: "share-123" }); rerender(); await waitFor(() => { expect(vi.mocked(sharingApi)).toHaveBeenCalled(); }); }); ``` ## Test Organization ### Comprehensive Test Coverage For hooks, ensure you test: - ✅ Initialization (with/without initial values) - ✅ Data fetching (success/error cases) - ✅ State updates and refetching - ✅ Dependency changes triggering effects - ✅ Manual actions (refetch, reset) - ✅ Race condition prevention - ✅ Cleanup on unmount - ✅ Mode switching (if applicable) - ✅ Edge cases (null/undefined data) ### Test Naming Use descriptive test names that explain the scenario: - ✅ "initializes with initial count" - ✅ "fetches response count on mount for regular survey" - ✅ "cancels previous request when new request is made" - ❌ "test hook" - ❌ "it works"