Skip to content

Commit

Permalink
test: add msw library for mocking API requests (#5492)
Browse files Browse the repository at this point in the history
Signed-off-by: Peter Makowski <[email protected]>
  • Loading branch information
petermakowski authored Jun 27, 2024
1 parent 8c17eb1 commit 2c64499
Show file tree
Hide file tree
Showing 7 changed files with 289 additions and 15 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@
"jsdom": "24.0.0",
"mock-socket": "9.3.1",
"mockdate": "3.0.5",
"msw": "2.3.1",
"nanoid": "5.0.7",
"nodemon": "3.1.3",
"npm-package-json-lint": "7.1.0",
Expand Down
6 changes: 5 additions & 1 deletion src/app/api/base.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { it, expect, vi, type Mock } from "vitest";

import { DEFAULT_HEADERS, fetchWithAuth } from "./base";
import { DEFAULT_HEADERS, fetchWithAuth, getFullApiUrl } from "./base";

import { getCookie } from "@/app/utils";

Expand Down Expand Up @@ -58,3 +58,7 @@ it("should handle invalid CSRF token", async () => {
headers: { ...DEFAULT_HEADERS, "X-CSRFToken": "" },
});
});

it("should generate correct full API URL", () => {
expect(getFullApiUrl("zones")).toBe("/MAAS/a/v2/zones");
});
12 changes: 11 additions & 1 deletion src/app/api/base.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { SERVICE_API } from "@/app/base/sagas/http";
import { getCookie } from "@/app/utils";

export const ROOT_API = "/MAAS/api/2.0/";
export const API_ENDPOINTS = {
zones: "zones",
} as const;

export const DEFAULT_HEADERS = {
"Content-Type": "application/json",
Expand All @@ -14,6 +17,13 @@ export const handleErrors = (response: Response) => {
return response;
};

type ApiEndpoint = typeof API_ENDPOINTS;
type ApiEndpointKey = keyof ApiEndpoint;
type ApiUrl = `${typeof SERVICE_API}${ApiEndpoint[ApiEndpointKey]}`;

export const getFullApiUrl = (endpoint: ApiEndpointKey): ApiUrl =>
`${SERVICE_API}${API_ENDPOINTS[endpoint]}`;

export const fetchWithAuth = async (url: string, options: RequestInit = {}) => {
const csrftoken = getCookie("csrftoken");
const headers = {
Expand Down
4 changes: 2 additions & 2 deletions src/app/api/endpoints.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ROOT_API, fetchWithAuth } from "@/app/api/base";
import { fetchWithAuth, getFullApiUrl } from "@/app/api/base";
import type { Zone } from "@/app/store/zone/types";

export const fetchZones = (): Promise<Zone[]> =>
fetchWithAuth(`${ROOT_API}zones/`);
fetchWithAuth(getFullApiUrl("zones"));
36 changes: 36 additions & 0 deletions src/app/api/query/zones.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { JsonBodyType } from "msw";

import { useZonesCount } from "./zones";

import { getFullApiUrl } from "@/app/api/base";
import * as factory from "@/testing/factories";
import {
renderHookWithQueryClient,
setupMockServer,
waitFor,
} from "@/testing/utils";

const { server, http, HttpResponse } = setupMockServer();

const setupZonesTest = (mockData: JsonBodyType) => {
server.use(
http.get(getFullApiUrl("zones"), () => HttpResponse.json(mockData))
);
return renderHookWithQueryClient(() => useZonesCount());
};

it("should return 0 when zones data is undefined", async () => {
const { result } = setupZonesTest(null);
await waitFor(() => expect(result.current).toBe(0));
});

it("should return the correct count when zones data is available", async () => {
const mockZonesData = [factory.zone(), factory.zone(), factory.zone()];
const { result } = setupZonesTest(mockZonesData);
await waitFor(() => expect(result.current).toBe(3));
});

it("should return 0 when zones data is an empty array", async () => {
const { result } = setupZonesTest([]);
await waitFor(() => expect(result.current).toBe(0));
});
53 changes: 44 additions & 9 deletions src/testing/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import type { RenderOptions, RenderResult } from "@testing-library/react";
import { render, screen, renderHook } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { produce } from "immer";
import { http, HttpResponse } from "msw";
import { setupServer } from "msw/node";
import { Provider } from "react-redux";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import type { MockStoreEnhanced } from "redux-mock-store";
Expand Down Expand Up @@ -41,13 +43,19 @@ import {
zoneState as zoneStateFactory,
} from "@/testing/factories";

const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
export const setupQueryClient = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
},
});
});
beforeEach(() => {
queryClient.resetQueries();
});
return queryClient;
};

/**
* Replace objects in an array with objects that have new values, given a match
Expand Down Expand Up @@ -128,7 +136,7 @@ export const BrowserRouterWithProvider = ({

const route = <Route element={children} path={routePattern} />;
return (
<QueryClientProvider client={queryClient}>
<QueryClientProvider client={setupQueryClient()}>
<Provider store={store ?? getMockStore(state || rootStateFactory())}>
<SidePanelContextProvider
initialSidePanelContent={sidePanelContent}
Expand Down Expand Up @@ -163,7 +171,7 @@ const WithMockStoreProvider = ({
return mockStore(state);
};
return (
<QueryClientProvider client={queryClient}>
<QueryClientProvider client={setupQueryClient()}>
<Provider store={store ?? getMockStore(state || rootStateFactory())}>
<SidePanelContextProvider>{children}</SidePanelContextProvider>
</Provider>
Expand Down Expand Up @@ -212,7 +220,7 @@ export const renderWithMockStore = (

const rendered = render(ui, {
wrapper: (props) => (
<QueryClientProvider client={queryClient}>
<QueryClientProvider client={setupQueryClient()}>
<WithMockStoreProvider {...props} state={initialState} store={store} />
</QueryClientProvider>
),
Expand Down Expand Up @@ -350,6 +358,33 @@ export const renderHookWithMockStore = (
return renderHook(hook, { wrapper: generateWrapper(store) });
};

export const renderHookWithQueryClient = (hook: Hook) => {
const queryClient = setupQueryClient();
return renderHook(hook, {
wrapper: ({ children }) => (
<QueryClientProvider client={queryClient}>
<Provider store={configureStore()(rootStateFactory())}>
{children}
</Provider>
</QueryClientProvider>
),
});
};

export const setupMockServer = () => {
const server = setupServer();

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

return {
server,
http,
HttpResponse,
};
};

export const waitFor = vi.waitFor;
export {
act,
Expand Down
Loading

0 comments on commit 2c64499

Please sign in to comment.