Skip to content

Commit e95f472

Browse files
authored
Centralized Mock Context Factory for GraphQL Resolver Unit Tests (#3319)
* mockfactoryFunction created * implemented mockFactory function to existing tests * format fixes * testing documentation updated * fixed naming conventions * fixed naming conventions + typos * code quality fixes
1 parent 6152d31 commit e95f472

File tree

16 files changed

+761
-748
lines changed

16 files changed

+761
-748
lines changed

docs/docs/docs/developer-resources/testing.md

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,130 @@ The `tests/server.ts` file exports the Talawa API server instance that can be im
5959

6060
There aren't any other strict structure requirements for the this directory.
6161

62+
### Mock GraphQL Context Factory Function
63+
64+
#### In Directory `test/_Mocks_/mockContextCreator`
65+
66+
#### **Purpose**
67+
68+
The `createMockGraphQLContext` function provides a **fully mocked GraphQL context** for unit and integration testing of GraphQL resolvers. It ensures that resolvers can be tested **without needing a real database, MinIO storage, or authentication service** and works as a centralized mocking mechanism.
69+
70+
#### **Usage**
71+
72+
#### **Importing the Mock Context**
73+
74+
```ts
75+
import { createMockGraphQLContext } from "test/_Mocks_/mockContextCreator";
76+
```
77+
78+
#### **Creating a Mock Context**
79+
80+
##### **For an Unauthenticated User**
81+
82+
```ts
83+
const { context, mocks } = createMockGraphQLContext({ isAuthenticated: false });
84+
```
85+
86+
`context.currentClient.isAuthenticated` will be `false`.
87+
88+
##### **For an Authenticated User**
89+
90+
```ts
91+
const { context, mocks } = createMockGraphQLContext({
92+
isAuthenticated: true,
93+
userId: "user123",
94+
});
95+
```
96+
97+
`context.currentClient.user.id` will be `"user123"`.
98+
99+
---
100+
101+
#### **Components in Mock Context**
102+
103+
The mock context provides the following:
104+
105+
| Component | Description |
106+
| --------------- | --------------------------------------------------------------------- |
107+
| `currentClient` | Simulates authenticated/unauthenticated users. |
108+
| `drizzleClient` | Mocked database client (`createMockDrizzleClient`). |
109+
| `envConfig` | Mocked environment variables (`API_BASE_URL`). |
110+
| `jwt.sign` | Mocked JWT generator (`vi.fn()` returning a test token). |
111+
| `log` | Mocked logger (`createMockLogger`). |
112+
| `minio` | Mocked MinIO client for object storage (`createMockMinioClient`). |
113+
| `pubsub` | Mocked pub-sub system for GraphQL subscriptions (`createMockPubSub`). |
114+
115+
---
116+
117+
#### **Return Value**
118+
119+
The function returns an object with two properties:
120+
121+
| Property | Description |
122+
| --------- | ------------------------------------------------------------------- |
123+
| `context` | The complete mocked GraphQL context to pass to resolvers |
124+
| `mocks` | Direct access to individual mock instances for setting expectations |
125+
126+
---
127+
128+
### **How Contributors Should Use It**
129+
130+
#### **Unit Testing Resolvers** (With exposed mocks for verification)
131+
132+
```ts
133+
test("should return user data", async () => {
134+
// Create context with mocks
135+
const { context, mocks } = createMockGraphQLContext({
136+
isAuthenticated: true,
137+
userId: "user123",
138+
});
139+
140+
// Configure mock behavior if needed
141+
mocks.drizzleClient.query.mockResolvedValue([
142+
{ id: "user123", name: "Test User" },
143+
]);
144+
145+
// Call your resolver
146+
const result = await userResolver({}, {}, context);
147+
148+
// Verify results
149+
expect(result.id).toBe("user123");
150+
151+
// Verify interactions with dependencies
152+
expect(mocks.drizzleClient.query).toHaveBeenCalledWith(
153+
expect.stringContaining("SELECT"),
154+
expect.arrayContaining(["user123"])
155+
);
156+
});
157+
```
158+
159+
---
160+
161+
### **Key Benefits**
162+
163+
- **Exposed Mocks** – Direct access to mock instances for setting expectations and verifying calls.
164+
- **Type Safety** – Proper TypeScript typing for all mocked components.
165+
- **Scalable** – Any future changes in `GraphQLContext` can be updated in one place, ensuring a single source of truth.
166+
167+
### **Simplified Call Signature**
168+
169+
The function supports both simple and object-based parameter styles:
170+
171+
```ts
172+
// Legacy style (still supported)
173+
const { context, mocks } = createMockGraphQLContext(true, "user123");
174+
175+
// New object-based style (recommended)
176+
const { context, mocks } = createMockGraphQLContext({
177+
isAuthenticated: true,
178+
userId: "user123",
179+
});
180+
```
181+
182+
### **Future Considerations**
183+
184+
In the future, there might be a requirement to run some tests sequentially. When that moment arrives, separating sequential and parallel tests into separate directories and using separate Vitest configuration for them would be the best idea.
185+
62186
### Future Considerations
63187

64188
In the future there might be a requirement to run some tests sequentially. When that moment arrives separating sequential and parallel tests into separate directories and using separate vitest configuration for them would be the best idea.

test/_Mocks_/drizzleClientMock.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { type Mock, vi } from "vitest";
2+
import * as drizzleSchema from "~/src/drizzle/schema";
3+
4+
// table method mocks
5+
type TableMethods<T> = {
6+
findFirst: Mock<() => Promise<T | undefined>>;
7+
findMany: Mock<() => Promise<T[]>>;
8+
insert: Mock<() => Promise<T>>;
9+
update: Mock<() => Promise<T>>;
10+
delete: Mock<() => Promise<void>>;
11+
count: Mock<() => Promise<number>>;
12+
};
13+
14+
// QueryTables that match Drizzle’s expected structure
15+
type QueryTables = {
16+
[K in keyof typeof drizzleSchema]: TableMethods<Record<string, unknown>>;
17+
};
18+
19+
// Function to create table methods with default mocks
20+
function createTableMethods<T>(): TableMethods<T> {
21+
return {
22+
findFirst: vi.fn(() => Promise.resolve(undefined)),
23+
findMany: vi.fn(() => Promise.resolve([])),
24+
insert: vi.fn(),
25+
update: vi.fn(),
26+
delete: vi.fn(),
27+
count: vi.fn(() => Promise.resolve(0)),
28+
};
29+
}
30+
31+
export function createMockDrizzleClient() {
32+
const queryTables = Object.keys(drizzleSchema).reduce<QueryTables>(
33+
(acc, tableName) => {
34+
acc[tableName as keyof typeof drizzleSchema] = createTableMethods();
35+
return acc;
36+
},
37+
{} as QueryTables,
38+
);
39+
40+
return {
41+
query: queryTables,
42+
};
43+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import type { FastifyInstance } from "fastify";
2+
import { createMockLogger } from "test/utilities/mockLogger";
3+
import { type MockInstance, vi } from "vitest";
4+
import type {
5+
CurrentClient,
6+
ExplicitGraphQLContext,
7+
GraphQLContext,
8+
} from "~/src/graphql/context";
9+
import { createMockDrizzleClient } from "../drizzleClientMock";
10+
import { createMockMinioClient } from "../mockMinioClient";
11+
import { createMockPubSub } from "../pubsubMock";
12+
13+
/**
14+
* Mock for an **unauthenticated user**.
15+
*/
16+
const unauthenticatedClient: CurrentClient = {
17+
isAuthenticated: false,
18+
};
19+
20+
/**
21+
* Mock for an **authenticated user**.
22+
*/
23+
const authenticatedClient = (userId: string): CurrentClient => ({
24+
isAuthenticated: true,
25+
user: { id: userId },
26+
});
27+
28+
/**
29+
* Function to create a **mock GraphQL context** with exposed mock instances.
30+
* @param isAuthenticated - Whether the client is authenticated.
31+
* @param userId - The user ID (only for authenticated users).
32+
* @returns An object containing the context and exposed mocks for testing.
33+
*/
34+
export function createMockGraphQLContext(
35+
isAuthenticated = true,
36+
userId = "user123",
37+
) {
38+
// Create mock instances with proper typing
39+
const mockDrizzleClient = createMockDrizzleClient();
40+
const mockMinioClient = createMockMinioClient();
41+
const mockPubSub = createMockPubSub();
42+
const mockJwtSign = vi
43+
.fn()
44+
.mockImplementation(
45+
(payload) => `mocked.jwt.${JSON.stringify(payload)}.token`,
46+
);
47+
48+
// Create the explicit context
49+
const explicitContext: ExplicitGraphQLContext = {
50+
currentClient: isAuthenticated
51+
? authenticatedClient(userId)
52+
: unauthenticatedClient,
53+
drizzleClient:
54+
mockDrizzleClient as unknown as FastifyInstance["drizzleClient"],
55+
envConfig: { API_BASE_URL: "http://localhost:4000" },
56+
jwt: {
57+
sign: mockJwtSign,
58+
},
59+
log: createMockLogger(),
60+
minio: mockMinioClient,
61+
};
62+
63+
// Create the implicit context
64+
const implicitContext = { pubsub: mockPubSub };
65+
66+
// Combine them into the full context
67+
const context: GraphQLContext = {
68+
...explicitContext,
69+
...implicitContext,
70+
};
71+
72+
// Return both the context and exposed mocks for easier testing
73+
return {
74+
context,
75+
mocks: {
76+
drizzleClient: mockDrizzleClient,
77+
minioClient: mockMinioClient,
78+
pubsub: mockPubSub,
79+
jwtSign: mockJwtSign as MockInstance,
80+
},
81+
};
82+
}

test/_Mocks_/mockMinioClient.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { Readable } from "node:stream";
2+
import { Client as MinioClient } from "minio";
3+
import { vi } from "vitest";
4+
5+
/**
6+
* Mocked MinIO Client Configuration
7+
*/
8+
const mockMinioConfig = {
9+
endPoint: "localhost",
10+
port: 9000,
11+
bucketName: "talawa" as const,
12+
};
13+
14+
/**
15+
* Type definition for the mock MinIO client
16+
*/
17+
type MockMinioClient = {
18+
client: MinioClient;
19+
config: typeof mockMinioConfig;
20+
bucketName: typeof mockMinioConfig.bucketName;
21+
};
22+
23+
/**
24+
* Creates a full mock MinIO client with all required methods
25+
*/
26+
export const createMockMinioClient = (): MockMinioClient => {
27+
// Creates a fully mocked MinIO client with all required methods
28+
const mockClient = new MinioClient({
29+
endPoint: mockMinioConfig.endPoint,
30+
port: mockMinioConfig.port,
31+
accessKey: "mock-access-key",
32+
secretKey: "mock-secret-key",
33+
useSSL: false,
34+
});
35+
36+
// Mocks the necessary methods
37+
mockClient.bucketExists = vi.fn(
38+
async (bucketName: string) => bucketName === mockMinioConfig.bucketName,
39+
);
40+
mockClient.makeBucket = vi.fn(async () => Promise.resolve());
41+
mockClient.listBuckets = vi.fn(async () => [
42+
{ name: mockMinioConfig.bucketName, creationDate: new Date() },
43+
]);
44+
mockClient.putObject = vi.fn(async () => ({
45+
etag: "mock-etag",
46+
versionId: null,
47+
}));
48+
mockClient.getObject = vi.fn(async (bucketName, objectName) => {
49+
if (bucketName !== mockMinioConfig.bucketName || !objectName) {
50+
throw new Error("Object not found");
51+
}
52+
const stream = new Readable({
53+
read() {
54+
this.push("mock file content");
55+
this.push(null);
56+
},
57+
});
58+
return stream;
59+
});
60+
mockClient.removeObject = vi.fn(async () => Promise.resolve());
61+
62+
return {
63+
bucketName: mockMinioConfig.bucketName,
64+
config: mockMinioConfig,
65+
client: mockClient,
66+
};
67+
};

test/_Mocks_/pubsubMock.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Readable } from "node:stream";
2+
import { vi } from "vitest";
3+
import type { PubSub } from "~/src/graphql/pubsub";
4+
5+
export function createMockPubSub(): PubSub {
6+
return {
7+
publish: vi.fn(),
8+
subscribe: vi.fn().mockResolvedValue(
9+
Object.assign(
10+
new Readable({
11+
read() {}, // Empty Readable Stream (mocks real event streams)
12+
}),
13+
{ [Symbol.asyncIterator]: async function* () {} },
14+
),
15+
),
16+
};
17+
}

0 commit comments

Comments
 (0)