Skip to content
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

Ability to dynamically match mocks #6701

Merged
merged 20 commits into from
Sep 19, 2023
Merged
Show file tree
Hide file tree
Changes from 17 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
2 changes: 1 addition & 1 deletion .api-reports/api-report-core.md
Original file line number Diff line number Diff line change
Expand Up @@ -1642,7 +1642,7 @@ class QueryInfo {
// Warning: (ae-forgotten-export) The symbol "CacheWriteBehavior" needs to be exported by the entry point index.d.ts
//
// (undocumented)
markResult<T>(result: FetchResult<T>, document: DocumentNode, options: Pick<WatchQueryOptions, "variables" | "fetchPolicy" | "errorPolicy">, cacheWriteBehavior: CacheWriteBehavior): void;
markResult<T>(result: FetchResult<T>, document: DocumentNode, options: Pick<WatchQueryOptions, "variables" | "fetchPolicy" | "errorPolicy">, cacheWriteBehavior: CacheWriteBehavior): typeof result;
// (undocumented)
networkError?: Error | null;
// (undocumented)
Expand Down
2 changes: 1 addition & 1 deletion .api-reports/api-report-react.md
Original file line number Diff line number Diff line change
Expand Up @@ -1463,7 +1463,7 @@ class QueryInfo {
// Warning: (ae-forgotten-export) The symbol "CacheWriteBehavior" needs to be exported by the entry point index.d.ts
//
// (undocumented)
markResult<T>(result: FetchResult<T>, document: DocumentNode, options: Pick<WatchQueryOptions, "variables" | "fetchPolicy" | "errorPolicy">, cacheWriteBehavior: CacheWriteBehavior): void;
markResult<T>(result: FetchResult<T>, document: DocumentNode, options: Pick<WatchQueryOptions, "variables" | "fetchPolicy" | "errorPolicy">, cacheWriteBehavior: CacheWriteBehavior): typeof result;
// (undocumented)
networkError?: Error | null;
// (undocumented)
Expand Down
2 changes: 1 addition & 1 deletion .api-reports/api-report-react_components.md
Original file line number Diff line number Diff line change
Expand Up @@ -1265,7 +1265,7 @@ class QueryInfo {
// Warning: (ae-forgotten-export) The symbol "CacheWriteBehavior" needs to be exported by the entry point index.d.ts
//
// (undocumented)
markResult<T>(result: FetchResult<T>, document: DocumentNode, options: Pick<WatchQueryOptions, "variables" | "fetchPolicy" | "errorPolicy">, cacheWriteBehavior: CacheWriteBehavior): void;
markResult<T>(result: FetchResult<T>, document: DocumentNode, options: Pick<WatchQueryOptions, "variables" | "fetchPolicy" | "errorPolicy">, cacheWriteBehavior: CacheWriteBehavior): typeof result;
// (undocumented)
networkError?: Error | null;
// (undocumented)
Expand Down
2 changes: 1 addition & 1 deletion .api-reports/api-report-react_context.md
Original file line number Diff line number Diff line change
Expand Up @@ -1175,7 +1175,7 @@ class QueryInfo {
// Warning: (ae-forgotten-export) The symbol "CacheWriteBehavior" needs to be exported by the entry point index.d.ts
//
// (undocumented)
markResult<T>(result: FetchResult<T>, document: DocumentNode, options: Pick<WatchQueryOptions, "variables" | "fetchPolicy" | "errorPolicy">, cacheWriteBehavior: CacheWriteBehavior): void;
markResult<T>(result: FetchResult<T>, document: DocumentNode, options: Pick<WatchQueryOptions, "variables" | "fetchPolicy" | "errorPolicy">, cacheWriteBehavior: CacheWriteBehavior): typeof result;
// (undocumented)
networkError?: Error | null;
// (undocumented)
Expand Down
2 changes: 1 addition & 1 deletion .api-reports/api-report-react_hoc.md
Original file line number Diff line number Diff line change
Expand Up @@ -1243,7 +1243,7 @@ class QueryInfo {
// Warning: (ae-forgotten-export) The symbol "CacheWriteBehavior" needs to be exported by the entry point index.d.ts
//
// (undocumented)
markResult<T>(result: FetchResult<T>, document: DocumentNode, options: Pick<WatchQueryOptions, "variables" | "fetchPolicy" | "errorPolicy">, cacheWriteBehavior: CacheWriteBehavior): void;
markResult<T>(result: FetchResult<T>, document: DocumentNode, options: Pick<WatchQueryOptions, "variables" | "fetchPolicy" | "errorPolicy">, cacheWriteBehavior: CacheWriteBehavior): typeof result;
// (undocumented)
networkError?: Error | null;
// (undocumented)
Expand Down
2 changes: 1 addition & 1 deletion .api-reports/api-report-react_hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -1393,7 +1393,7 @@ class QueryInfo {
// Warning: (ae-forgotten-export) The symbol "CacheWriteBehavior" needs to be exported by the entry point index.d.ts
//
// (undocumented)
markResult<T>(result: FetchResult<T>, document: DocumentNode, options: Pick<WatchQueryOptions, "variables" | "fetchPolicy" | "errorPolicy">, cacheWriteBehavior: CacheWriteBehavior): void;
markResult<T>(result: FetchResult<T>, document: DocumentNode, options: Pick<WatchQueryOptions, "variables" | "fetchPolicy" | "errorPolicy">, cacheWriteBehavior: CacheWriteBehavior): typeof result;
// (undocumented)
networkError?: Error | null;
// (undocumented)
Expand Down
2 changes: 1 addition & 1 deletion .api-reports/api-report-react_ssr.md
Original file line number Diff line number Diff line change
Expand Up @@ -1162,7 +1162,7 @@ class QueryInfo {
// Warning: (ae-forgotten-export) The symbol "CacheWriteBehavior" needs to be exported by the entry point index.d.ts
//
// (undocumented)
markResult<T>(result: FetchResult<T>, document: DocumentNode, options: Pick<WatchQueryOptions, "variables" | "fetchPolicy" | "errorPolicy">, cacheWriteBehavior: CacheWriteBehavior): void;
markResult<T>(result: FetchResult<T>, document: DocumentNode, options: Pick<WatchQueryOptions, "variables" | "fetchPolicy" | "errorPolicy">, cacheWriteBehavior: CacheWriteBehavior): typeof result;
// (undocumented)
networkError?: Error | null;
// (undocumented)
Expand Down
15 changes: 11 additions & 4 deletions .api-reports/api-report-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -882,7 +882,7 @@ interface MockedProviderState {
}

// @public (undocumented)
export interface MockedResponse<TData = Record<string, any>, TVariables = Record<string, any>> {
export interface MockedResponse<TData extends object = Record<string, any>, TVariables extends object = Record<string, any>> {
// (undocumented)
delay?: number;
// (undocumented)
Expand All @@ -892,7 +892,11 @@ export interface MockedResponse<TData = Record<string, any>, TVariables = Record
// (undocumented)
request: GraphQLRequest<TVariables>;
// (undocumented)
result?: FetchResult<TData> | ResultFunction<FetchResult<TData>>;
result?: FetchResult<TData> | ResultFunction<FetchResult<TData>, TVariables>;
// Warning: (ae-forgotten-export) The symbol "VariableMatcher" needs to be exported by the entry point index.d.ts
//
// (undocumented)
variableMatcher?: VariableMatcher<TVariables>;
}

// @public (undocumented)
Expand Down Expand Up @@ -1238,7 +1242,7 @@ class QueryInfo {
// Warning: (ae-forgotten-export) The symbol "CacheWriteBehavior" needs to be exported by the entry point index.d.ts
//
// (undocumented)
markResult<T>(result: FetchResult<T>, document: DocumentNode, options: Pick<WatchQueryOptions, "variables" | "fetchPolicy" | "errorPolicy">, cacheWriteBehavior: CacheWriteBehavior): void;
markResult<T>(result: FetchResult<T>, document: DocumentNode, options: Pick<WatchQueryOptions, "variables" | "fetchPolicy" | "errorPolicy">, cacheWriteBehavior: CacheWriteBehavior): typeof result;
// (undocumented)
networkError?: Error | null;
// (undocumented)
Expand Down Expand Up @@ -1497,7 +1501,7 @@ interface Resolvers {
}

// @public (undocumented)
export type ResultFunction<T> = () => T;
export type ResultFunction<T, V = Record<string, any>> = (variables: V) => T;

// @public (undocumented)
type SafeReadonly<T> = T extends object ? Readonly<T> : T;
Expand Down Expand Up @@ -1623,6 +1627,9 @@ interface UriFunction {
(operation: Operation): string;
}

// @public (undocumented)
type VariableMatcher<V = Record<string, any>> = (variables: V) => boolean;

// @public (undocumented)
export function wait(ms: number): Promise<void>;

Expand Down
15 changes: 11 additions & 4 deletions .api-reports/api-report-testing_core.md
Original file line number Diff line number Diff line change
Expand Up @@ -838,7 +838,7 @@ interface MockApolloLink extends ApolloLink {
}

// @public (undocumented)
export interface MockedResponse<TData = Record<string, any>, TVariables = Record<string, any>> {
export interface MockedResponse<TData extends object = Record<string, any>, TVariables extends object = Record<string, any>> {
// (undocumented)
delay?: number;
// (undocumented)
Expand All @@ -848,7 +848,11 @@ export interface MockedResponse<TData = Record<string, any>, TVariables = Record
// (undocumented)
request: GraphQLRequest<TVariables>;
// (undocumented)
result?: FetchResult<TData> | ResultFunction<FetchResult<TData>>;
result?: FetchResult<TData> | ResultFunction<FetchResult<TData>, TVariables>;
// Warning: (ae-forgotten-export) The symbol "VariableMatcher" needs to be exported by the entry point index.d.ts
//
// (undocumented)
variableMatcher?: VariableMatcher<TVariables>;
}

// @public (undocumented)
Expand Down Expand Up @@ -1194,7 +1198,7 @@ class QueryInfo {
// Warning: (ae-forgotten-export) The symbol "CacheWriteBehavior" needs to be exported by the entry point index.d.ts
//
// (undocumented)
markResult<T>(result: FetchResult<T>, document: DocumentNode, options: Pick<WatchQueryOptions, "variables" | "fetchPolicy" | "errorPolicy">, cacheWriteBehavior: CacheWriteBehavior): void;
markResult<T>(result: FetchResult<T>, document: DocumentNode, options: Pick<WatchQueryOptions, "variables" | "fetchPolicy" | "errorPolicy">, cacheWriteBehavior: CacheWriteBehavior): typeof result;
// (undocumented)
networkError?: Error | null;
// (undocumented)
Expand Down Expand Up @@ -1455,7 +1459,7 @@ interface Resolvers {
}

// @public (undocumented)
export type ResultFunction<T> = () => T;
export type ResultFunction<T, V = Record<string, any>> = (variables: V) => T;

// @public (undocumented)
type SafeReadonly<T> = T extends object ? Readonly<T> : T;
Expand Down Expand Up @@ -1581,6 +1585,9 @@ interface UriFunction {
(operation: Operation): string;
}

// @public (undocumented)
type VariableMatcher<V = Record<string, any>> = (variables: V) => boolean;

// @public (undocumented)
export function wait(ms: number): Promise<void>;

Expand Down
2 changes: 1 addition & 1 deletion .api-reports/api-report-utilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -1905,7 +1905,7 @@ class QueryInfo {
// Warning: (ae-forgotten-export) The symbol "CacheWriteBehavior" needs to be exported by the entry point index.d.ts
//
// (undocumented)
markResult<T>(result: FetchResult<T>, document: DocumentNode, options: Pick<WatchQueryOptions, "variables" | "fetchPolicy" | "errorPolicy">, cacheWriteBehavior: CacheWriteBehavior): void;
markResult<T>(result: FetchResult<T>, document: DocumentNode, options: Pick<WatchQueryOptions, "variables" | "fetchPolicy" | "errorPolicy">, cacheWriteBehavior: CacheWriteBehavior): typeof result;
// (undocumented)
networkError?: Error | null;
// (undocumented)
Expand Down
2 changes: 1 addition & 1 deletion .api-reports/api-report.md
Original file line number Diff line number Diff line change
Expand Up @@ -2017,7 +2017,7 @@ class QueryInfo {
// Warning: (ae-forgotten-export) The symbol "CacheWriteBehavior" needs to be exported by the entry point index.d.ts
//
// (undocumented)
markResult<T>(result: FetchResult<T>, document: DocumentNode, options: Pick<WatchQueryOptions, "variables" | "fetchPolicy" | "errorPolicy">, cacheWriteBehavior: CacheWriteBehavior): void;
markResult<T>(result: FetchResult<T>, document: DocumentNode, options: Pick<WatchQueryOptions, "variables" | "fetchPolicy" | "errorPolicy">, cacheWriteBehavior: CacheWriteBehavior): typeof result;
// (undocumented)
networkError?: Error | null;
// (undocumented)
Expand Down
7 changes: 7 additions & 0 deletions .changeset/sour-sheep-walk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@apollo/client": minor
---

Ability to dynamically match mocks

Adds support for a new property `MockedResponse.variableMatcher`: a predicate function that accepts a `variables` param. If `true`, the `variables` will be passed into the `ResultFunction` to help dynamically build a response.
38 changes: 37 additions & 1 deletion docs/source/development-testing/testing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ Each mock object defines a `request` field (indicating the shape and variables o
Alternatively, the `result` field can be a function that returns a mocked response after performing arbitrary logic:

```jsx
result: () => {
result: (variables) => { // `variables` is optional
// ...arbitrary logic...

return {
Expand Down Expand Up @@ -150,6 +150,42 @@ it("renders without error", async () => {

</ExpansionPanel>

### Dynamic variables
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file has been updated several times since this PR was first opened. Some updates were warranted, I tried not to take too many liberties. Critical feedback is welcome 🙏🏻


Sometimes, the exact value of the variables being passed are not known. The `MockedResponse` object takes a `variableMatcher` property that is a function that takes the variables and return a boolean indication if this mock should match the invocation for the provided query. You cannot specify this parameter and `request.variables` at the same time.

For example, this mock will match all dog queries:

```js
const dogMock = {
request: {
query: GET_DOG_QUERY
},
variableMatcher: (variables) => true,
result: {
data: { dog: { id: 1, name: 'Buck', breed: 'poodle' } },
},
};
```

This can also be useful for asserting specific variables individually:

```js
const dogMock = {
request: {
query: GET_DOG_QUERY
},
variableMatcher: jest.fn().mockReturnValue(true),
result: {
data: { dog: { id: 1, name: 'Buck', breed: 'poodle' } },
},
};

expect(variableMatcher).toHaveBeenCalledWith(expect.objectContaining({
name: 'Buck'
}));
```

### Setting `addTypename`

In the example above, we set the `addTypename` prop of `MockedProvider` to `false`. This prevents Apollo Client from automatically adding the special `__typename` field to every object it queries for (it does this by default to support data normalization in the cache).
Expand Down
38 changes: 32 additions & 6 deletions src/testing/core/mocking/mockLink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,21 @@ import {
print,
} from "../../../utilities/index.js";

export type ResultFunction<T> = () => T;
export type ResultFunction<T, V = Record<string, any>> = (variables: V) => T;

export type VariableMatcher<V = Record<string, any>> = (
variables: V
) => boolean;

export interface MockedResponse<
TData = Record<string, any>,
TVariables = Record<string, any>,
TData extends object = Record<string, any>,
TVariables extends object = Record<string, any>,
> {
request: GraphQLRequest<TVariables>;
result?: FetchResult<TData> | ResultFunction<FetchResult<TData>>;
result?: FetchResult<TData> | ResultFunction<FetchResult<TData>, TVariables>;
error?: Error;
delay?: number;
variableMatcher?: VariableMatcher<TVariables>;
newData?: ResultFunction<FetchResult>;
}

Expand Down Expand Up @@ -93,6 +98,9 @@ export class MockLink extends ApolloLink {
if (equal(requestVariables, mockedResponseVars)) {
return true;
}
if (res.variableMatcher && res.variableMatcher(operation.variables)) {
return true;
}
unmatchedVars.push(mockedResponseVars);
return false;
})
Expand Down Expand Up @@ -131,7 +139,7 @@ ${unmatchedVars.map((d) => ` ${stringifyForDisplay(d)}`).join("\n")}

const { newData } = response;
if (newData) {
response.result = newData();
response.result = newData(operation.variables);
mockedResponses.push(response);
}

Expand Down Expand Up @@ -165,7 +173,7 @@ ${unmatchedVars.map((d) => ` ${stringifyForDisplay(d)}`).join("\n")}
if (response.result) {
observer.next(
typeof response.result === "function"
? (response.result as ResultFunction<FetchResult>)()
? response.result(operation.variables)
: response.result
);
}
Expand Down Expand Up @@ -195,8 +203,26 @@ ${unmatchedVars.map((d) => ` ${stringifyForDisplay(d)}`).join("\n")}
if (query) {
newMockedResponse.request.query = query;
}
this.normalizeVariableMatching(newMockedResponse);
return newMockedResponse;
}

private normalizeVariableMatching(mockedResponse: MockedResponse) {
const variables = mockedResponse.request.variables;
if (mockedResponse.variableMatcher && variables) {
throw new Error(
"Mocked response should contain either variableMatcher or request.variables"
);
}

if (!mockedResponse.variableMatcher) {
mockedResponse.variableMatcher = (vars) => {
const requestVariables = vars || {};
const mockedResponseVariables = variables || {};
return equal(requestVariables, mockedResponseVariables);
};
}
}
}

export interface MockApolloLink extends ApolloLink {
Expand Down
Loading
Loading