Skip to content

Centralized Mock Context Factory for GraphQL Resolver Unit Tests #3319

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions docs/docs/docs/developer-resources/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,130 @@ The `tests/server.ts` file exports the Talawa API server instance that can be im

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

### Mock GraphQL Context Factory Function

#### In Directory `test/_Mocks_/mockContextCreator`

#### **Purpose**

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.

#### **Usage**

#### **Importing the Mock Context**

```ts
import { createMockGraphQLContext } from "test/_Mocks_/mockContextCreator";
```

#### **Creating a Mock Context**

##### **For an Unauthenticated User**

```ts
const { context, mocks } = createMockGraphQLContext({ isAuthenticated: false });
```

`context.currentClient.isAuthenticated` will be `false`.

##### **For an Authenticated User**

```ts
const { context, mocks } = createMockGraphQLContext({
isAuthenticated: true,
userId: "user123",
});
```

`context.currentClient.user.id` will be `"user123"`.

---

#### **Components in Mock Context**

The mock context provides the following:

| Component | Description |
| --------------- | --------------------------------------------------------------------- |
| `currentClient` | Simulates authenticated/unauthenticated users. |
| `drizzleClient` | Mocked database client (`createMockDrizzleClient`). |
| `envConfig` | Mocked environment variables (`API_BASE_URL`). |
| `jwt.sign` | Mocked JWT generator (`vi.fn()` returning a test token). |
| `log` | Mocked logger (`createMockLogger`). |
| `minio` | Mocked MinIO client for object storage (`createMockMinioClient`). |
| `pubsub` | Mocked pub-sub system for GraphQL subscriptions (`createMockPubSub`). |

---

#### **Return Value**

The function returns an object with two properties:

| Property | Description |
| --------- | ------------------------------------------------------------------- |
| `context` | The complete mocked GraphQL context to pass to resolvers |
| `mocks` | Direct access to individual mock instances for setting expectations |

---

### **How Contributors Should Use It**

#### **Unit Testing Resolvers** (With exposed mocks for verification)

```ts
test("should return user data", async () => {
// Create context with mocks
const { context, mocks } = createMockGraphQLContext({
isAuthenticated: true,
userId: "user123",
});

// Configure mock behavior if needed
mocks.drizzleClient.query.mockResolvedValue([
{ id: "user123", name: "Test User" },
]);

// Call your resolver
const result = await userResolver({}, {}, context);

// Verify results
expect(result.id).toBe("user123");

// Verify interactions with dependencies
expect(mocks.drizzleClient.query).toHaveBeenCalledWith(
expect.stringContaining("SELECT"),
expect.arrayContaining(["user123"])
);
});
```

---

### **Key Benefits**

- **Exposed Mocks** – Direct access to mock instances for setting expectations and verifying calls.
- **Type Safety** – Proper TypeScript typing for all mocked components.
- **Scalable** – Any future changes in `GraphQLContext` can be updated in one place, ensuring a single source of truth.

### **Simplified Call Signature**

The function supports both simple and object-based parameter styles:

```ts
// Legacy style (still supported)
const { context, mocks } = createMockGraphQLContext(true, "user123");

// New object-based style (recommended)
const { context, mocks } = createMockGraphQLContext({
isAuthenticated: true,
userId: "user123",
});
```

### **Future Considerations**

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.

### Future Considerations

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.
Expand Down
43 changes: 43 additions & 0 deletions test/_Mocks_/drizzleClientMock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { type Mock, vi } from "vitest";
import * as drizzleSchema from "~/src/drizzle/schema";

// table method mocks
type TableMethods<T> = {
findFirst: Mock<() => Promise<T | undefined>>;
findMany: Mock<() => Promise<T[]>>;
insert: Mock<() => Promise<T>>;
update: Mock<() => Promise<T>>;
delete: Mock<() => Promise<void>>;
count: Mock<() => Promise<number>>;
};

// QueryTables that match Drizzle’s expected structure
type QueryTables = {
[K in keyof typeof drizzleSchema]: TableMethods<Record<string, unknown>>;
};

// Function to create table methods with default mocks
function createTableMethods<T>(): TableMethods<T> {
return {
findFirst: vi.fn(() => Promise.resolve(undefined)),
findMany: vi.fn(() => Promise.resolve([])),
insert: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
count: vi.fn(() => Promise.resolve(0)),
};
}

export function createMockDrizzleClient() {
const queryTables = Object.keys(drizzleSchema).reduce<QueryTables>(
(acc, tableName) => {
acc[tableName as keyof typeof drizzleSchema] = createTableMethods();
return acc;
},
{} as QueryTables,
);

return {
query: queryTables,
};
}
82 changes: 82 additions & 0 deletions test/_Mocks_/mockContextCreator/mockContextCreator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import type { FastifyInstance } from "fastify";
import { createMockLogger } from "test/utilities/mockLogger";
import { type MockInstance, vi } from "vitest";
import type {
CurrentClient,
ExplicitGraphQLContext,
GraphQLContext,
} from "~/src/graphql/context";
import { createMockDrizzleClient } from "../drizzleClientMock";
import { createMockMinioClient } from "../mockMinioClient";
import { createMockPubSub } from "../pubsubMock";

/**
* Mock for an **unauthenticated user**.
*/
const unauthenticatedClient: CurrentClient = {
isAuthenticated: false,
};

/**
* Mock for an **authenticated user**.
*/
const authenticatedClient = (userId: string): CurrentClient => ({
isAuthenticated: true,
user: { id: userId },
});

/**
* Function to create a **mock GraphQL context** with exposed mock instances.
* @param isAuthenticated - Whether the client is authenticated.
* @param userId - The user ID (only for authenticated users).
* @returns An object containing the context and exposed mocks for testing.
*/
export function createMockGraphQLContext(
isAuthenticated = true,
userId = "user123",
) {
// Create mock instances with proper typing
const mockDrizzleClient = createMockDrizzleClient();
const mockMinioClient = createMockMinioClient();
const mockPubSub = createMockPubSub();
const mockJwtSign = vi
.fn()
.mockImplementation(
(payload) => `mocked.jwt.${JSON.stringify(payload)}.token`,
);

// Create the explicit context
const explicitContext: ExplicitGraphQLContext = {
currentClient: isAuthenticated
? authenticatedClient(userId)
: unauthenticatedClient,
drizzleClient:
mockDrizzleClient as unknown as FastifyInstance["drizzleClient"],
envConfig: { API_BASE_URL: "http://localhost:4000" },
jwt: {
sign: mockJwtSign,
},
log: createMockLogger(),
minio: mockMinioClient,
};

// Create the implicit context
const implicitContext = { pubsub: mockPubSub };

// Combine them into the full context
const context: GraphQLContext = {
...explicitContext,
...implicitContext,
};

// Return both the context and exposed mocks for easier testing
return {
context,
mocks: {
drizzleClient: mockDrizzleClient,
minioClient: mockMinioClient,
pubsub: mockPubSub,
jwtSign: mockJwtSign as MockInstance,
},
};
}
67 changes: 67 additions & 0 deletions test/_Mocks_/mockMinioClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Readable } from "node:stream";
import { Client as MinioClient } from "minio";
import { vi } from "vitest";

/**
* Mocked MinIO Client Configuration
*/
const mockMinioConfig = {
endPoint: "localhost",
port: 9000,
bucketName: "talawa" as const,
};

/**
* Type definition for the mock MinIO client
*/
type MockMinioClient = {
client: MinioClient;
config: typeof mockMinioConfig;
bucketName: typeof mockMinioConfig.bucketName;
};

/**
* Creates a full mock MinIO client with all required methods
*/
export const createMockMinioClient = (): MockMinioClient => {
// Creates a fully mocked MinIO client with all required methods
const mockClient = new MinioClient({
endPoint: mockMinioConfig.endPoint,
port: mockMinioConfig.port,
accessKey: "mock-access-key",
secretKey: "mock-secret-key",
useSSL: false,
});

// Mocks the necessary methods
mockClient.bucketExists = vi.fn(
async (bucketName: string) => bucketName === mockMinioConfig.bucketName,
);
mockClient.makeBucket = vi.fn(async () => Promise.resolve());
mockClient.listBuckets = vi.fn(async () => [
{ name: mockMinioConfig.bucketName, creationDate: new Date() },
]);
mockClient.putObject = vi.fn(async () => ({
etag: "mock-etag",
versionId: null,
}));
mockClient.getObject = vi.fn(async (bucketName, objectName) => {
if (bucketName !== mockMinioConfig.bucketName || !objectName) {
throw new Error("Object not found");
}
const stream = new Readable({
read() {
this.push("mock file content");
this.push(null);
},
});
return stream;
});
mockClient.removeObject = vi.fn(async () => Promise.resolve());

return {
bucketName: mockMinioConfig.bucketName,
config: mockMinioConfig,
client: mockClient,
};
};
17 changes: 17 additions & 0 deletions test/_Mocks_/pubsubMock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Readable } from "node:stream";
import { vi } from "vitest";
import type { PubSub } from "~/src/graphql/pubsub";

export function createMockPubSub(): PubSub {
return {
publish: vi.fn(),
subscribe: vi.fn().mockResolvedValue(
Object.assign(
new Readable({
read() {}, // Empty Readable Stream (mocks real event streams)
}),
{ [Symbol.asyncIterator]: async function* () {} },
),
),
};
}
Loading
Loading