Skip to content

Commit 315faf9

Browse files
authoredFeb 3, 2023
Log unmatched mocks to the console for tests (#10502)
* Log a warning to the console when there are unmatched mocks in MockLink for a request. * Silence expected warnings in other MockedProvider tests. * Update docs to document the `silenceWarnings` option. * Add a changeset * Improve warning message displayed in console for unmatched mocks. * Rename silenceWarnings to showWarnings * Minor adjustment to changeset wording * Fix default value reference in docs for showWarnings * Fix broken test due to change in warnings shown to console for MockLink
1 parent 14a56b1 commit 315faf9

File tree

8 files changed

+218
-18
lines changed

8 files changed

+218
-18
lines changed
 

‎.changeset/funny-files-suffer.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@apollo/client': patch
3+
---
4+
5+
Log a warning to the console when a mock passed to `MockedProvider` or `MockLink` cannot be matched to a query during a test. This makes it easier to debug user errors in the mock setup, such as typos, especially if the query under test is using an `errorPolicy` set to `ignore`, which makes it difficult to know that a match did not occur.

‎docs/source/api/react/testing.md

+16
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,22 @@ Props to pass down to the `MockedProvider`'s child.
119119
</td>
120120
</tr>
121121

122+
<tr>
123+
<td>
124+
125+
###### `showWarnings`
126+
127+
`boolean`
128+
</td>
129+
<td>
130+
131+
When a request fails to match a mock, a warning is logged to the console to indicate the mismatch. Set this to `false` to silence these warnings.
132+
133+
The default value is `true`.
134+
135+
</td>
136+
</tr>
137+
122138
</tbody>
123139
</table>
124140

‎docs/source/development-testing/testing.mdx

+10-3
Original file line numberDiff line numberDiff line change
@@ -394,12 +394,19 @@ In order to properly test local state using `MockedProvider`, you'll need to pas
394394
`MockedProvider` creates its own ApolloClient instance behind the scenes like this:
395395

396396
```jsx
397-
const { mocks, addTypename, defaultOptions, cache, resolvers, link } =
398-
this.props;
397+
const {
398+
mocks,
399+
addTypename,
400+
defaultOptions,
401+
cache,
402+
resolvers,
403+
link,
404+
showWarnings,
405+
} = this.props;
399406
const client = new ApolloClient({
400407
cache: cache || new Cache({ addTypename }),
401408
defaultOptions,
402-
link: link || new MockLink(mocks || [], addTypename),
409+
link: link || new MockLink(mocks || [], addTypename, { showWarnings }),
403410
resolvers,
404411
});
405412
```

‎src/core/__tests__/ObservableQuery.ts

+21-7
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { ApolloLink } from '../../link/core';
1515
import { InMemoryCache, NormalizedCacheObject } from '../../cache';
1616
import { ApolloError } from '../../errors';
1717

18-
import { itAsync, mockSingleLink, subscribeAndCount } from '../../testing';
18+
import { itAsync, MockLink, mockSingleLink, subscribeAndCount } from '../../testing';
1919
import mockQueryManager from '../../testing/core/mocking/mockQueryManager';
2020
import mockWatchQuery from '../../testing/core/mocking/mockWatchQuery';
2121
import wrap from '../../testing/core/wrap';
@@ -1509,11 +1509,25 @@ describe('ObservableQuery', () => {
15091509
};
15101510
}
15111511

1512-
const observableWithVarsVar: ObservableQuery<any> = mockWatchQuery(
1513-
reject,
1514-
makeMock("a", "b", "c"),
1515-
makeMock("d", "e"),
1516-
);
1512+
// We construct the queryManager manually here rather than using
1513+
// `mockWatchQuery` because we need to silence console warnings for
1514+
// unmatched variables since. This test checks for calls to
1515+
// `console.warn` and unfortunately `mockSingleLink` (used by
1516+
// `mockWatchQuery`) does not support the ability to disable warnings
1517+
// without introducing a breaking change. Instead we construct this
1518+
// manually to be able to turn off warnings for this test.
1519+
const mocks = [makeMock('a', 'b', 'c'), makeMock('d', 'e')];
1520+
const firstRequest = mocks[0].request;
1521+
const queryManager = new QueryManager({
1522+
cache: new InMemoryCache({ addTypename: false }),
1523+
link: new MockLink(mocks, true, { showWarnings: false })
1524+
})
1525+
1526+
const observableWithVarsVar = queryManager.watchQuery({
1527+
query: firstRequest.query,
1528+
variables: firstRequest.variables,
1529+
notifyOnNetworkStatusChange: false
1530+
});
15171531

15181532
subscribeAndCount(error => {
15191533
expect(error.message).toMatch(
@@ -1536,7 +1550,7 @@ describe('ObservableQuery', () => {
15361550
// to call refetch(variables).
15371551
observableWithVarsVar.refetch({
15381552
variables: { vars: ["d", "e"] },
1539-
}).then(result => {
1553+
} as any).then(result => {
15401554
reject(`unexpected result ${JSON.stringify(result)}; should have thrown`);
15411555
}, error => {
15421556
expect(error.message).toMatch(

‎src/testing/core/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export {
22
MockLink,
33
mockSingleLink,
44
MockedResponse,
5+
MockLinkOptions,
56
ResultFunction
67
} from './mocking/mockLink';
78
export {

‎src/testing/core/mocking/mockLink.ts

+17-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ export interface MockedResponse<TData = Record<string, any>> {
2929
newData?: ResultFunction<FetchResult>;
3030
}
3131

32+
export interface MockLinkOptions {
33+
showWarnings?: boolean;
34+
}
35+
3236
function requestToKey(request: GraphQLRequest, addTypename: Boolean): string {
3337
const queryString =
3438
request.query &&
@@ -40,14 +44,18 @@ function requestToKey(request: GraphQLRequest, addTypename: Boolean): string {
4044
export class MockLink extends ApolloLink {
4145
public operation: Operation;
4246
public addTypename: Boolean = true;
47+
public showWarnings: boolean = true;
4348
private mockedResponsesByKey: { [key: string]: MockedResponse[] } = {};
4449

4550
constructor(
4651
mockedResponses: ReadonlyArray<MockedResponse>,
47-
addTypename: Boolean = true
52+
addTypename: Boolean = true,
53+
options: MockLinkOptions = Object.create(null)
4854
) {
4955
super();
5056
this.addTypename = addTypename;
57+
this.showWarnings = options.showWarnings ?? true;
58+
5159
if (mockedResponses) {
5260
mockedResponses.forEach(mockedResponse => {
5361
this.addMockedResponse(mockedResponse);
@@ -102,6 +110,14 @@ Failed to match ${unmatchedVars.length} mock${
102110
} for this query. The mocked response had the following variables:
103111
${unmatchedVars.map(d => ` ${stringifyForDisplay(d)}`).join('\n')}
104112
` : ""}`);
113+
114+
if (this.showWarnings) {
115+
console.warn(
116+
configError.message +
117+
'\nThis typically indicates a configuration error in your mocks ' +
118+
'setup, usually due to a typo or mismatched variable.'
119+
);
120+
}
105121
} else {
106122
mockedResponses.splice(responseIndex, 1);
107123

‎src/testing/react/MockedProvider.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export interface MockedProviderProps<TSerializedCache = {}> {
1717
childProps?: object;
1818
children?: any;
1919
link?: ApolloLink;
20+
showWarnings?: boolean;
2021
}
2122

2223
export interface MockedProviderState {
@@ -40,14 +41,16 @@ export class MockedProvider extends React.Component<
4041
defaultOptions,
4142
cache,
4243
resolvers,
43-
link
44+
link,
45+
showWarnings,
4446
} = this.props;
4547
const client = new ApolloClient({
4648
cache: cache || new Cache({ addTypename }),
4749
defaultOptions,
4850
link: link || new MockLink(
4951
mocks || [],
5052
addTypename,
53+
{ showWarnings }
5154
),
5255
resolvers,
5356
});

‎src/testing/react/__tests__/MockedProvider.test.tsx

+144-6
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ describe('General use', () => {
175175
};
176176

177177
render(
178-
<MockedProvider mocks={mocks}>
178+
<MockedProvider showWarnings={false} mocks={mocks}>
179179
<Component {...variables2} />
180180
</MockedProvider>
181181
);
@@ -215,7 +215,7 @@ describe('General use', () => {
215215
};
216216

217217
render(
218-
<MockedProvider mocks={mocks2}>
218+
<MockedProvider showWarnings={false} mocks={mocks2}>
219219
<Component {...variables2} />
220220
</MockedProvider>
221221
);
@@ -325,7 +325,7 @@ describe('General use', () => {
325325
];
326326

327327
render(
328-
<MockedProvider mocks={mocksDifferentQuery}>
328+
<MockedProvider showWarnings={false} mocks={mocksDifferentQuery}>
329329
<Component {...variables} />
330330
</MockedProvider>
331331
);
@@ -435,7 +435,10 @@ describe('General use', () => {
435435
return null;
436436
}
437437

438-
const link = ApolloLink.from([errorLink, new MockLink([])]);
438+
const link = ApolloLink.from([
439+
errorLink,
440+
new MockLink([], true, { showWarnings: false })
441+
]);
439442

440443
render(
441444
<MockedProvider link={link}>
@@ -485,14 +488,149 @@ describe('General use', () => {
485488
expect(errorThrown).toBeFalsy();
486489
});
487490

491+
it('shows a warning in the console when there is no matched mock', async () => {
492+
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
493+
let finished = false;
494+
function Component({ ...variables }: Variables) {
495+
const { loading } = useQuery<Data, Variables>(query, { variables });
496+
if (!loading) {
497+
finished = true;
498+
}
499+
return null;
500+
}
501+
502+
const mocksDifferentQuery = [
503+
{
504+
request: {
505+
query: gql`
506+
query OtherQuery {
507+
otherQuery {
508+
id
509+
}
510+
}
511+
`,
512+
variables
513+
},
514+
result: { data: { user } }
515+
}
516+
];
517+
518+
render(
519+
<MockedProvider mocks={mocksDifferentQuery}>
520+
<Component {...variables} />
521+
</MockedProvider>
522+
);
523+
524+
await waitFor(() => {
525+
expect(finished).toBe(true);
526+
});
527+
528+
expect(console.warn).toHaveBeenCalledTimes(1);
529+
expect(console.warn).toHaveBeenCalledWith(
530+
expect.stringContaining('No more mocked responses for the query')
531+
);
532+
533+
consoleSpy.mockRestore();
534+
});
535+
536+
it('silences console warning for unmatched mocks when `showWarnings` is `false`', async () => {
537+
const consoleSpy = jest.spyOn(console, 'warn');
538+
let finished = false;
539+
function Component({ ...variables }: Variables) {
540+
const { loading } = useQuery<Data, Variables>(query, { variables });
541+
if (!loading) {
542+
finished = true;
543+
}
544+
return null;
545+
}
546+
547+
const mocksDifferentQuery = [
548+
{
549+
request: {
550+
query: gql`
551+
query OtherQuery {
552+
otherQuery {
553+
id
554+
}
555+
}
556+
`,
557+
variables
558+
},
559+
result: { data: { user } }
560+
}
561+
];
562+
563+
render(
564+
<MockedProvider mocks={mocksDifferentQuery} showWarnings={false}>
565+
<Component {...variables} />
566+
</MockedProvider>
567+
);
568+
569+
await waitFor(() => {
570+
expect(finished).toBe(true);
571+
});
572+
573+
expect(console.warn).not.toHaveBeenCalled();
574+
575+
consoleSpy.mockRestore();
576+
});
577+
578+
it('silences console warning for unmatched mocks when passing `showWarnings` to `MockLink` directly', async () => {
579+
const consoleSpy = jest.spyOn(console, 'warn');
580+
let finished = false;
581+
function Component({ ...variables }: Variables) {
582+
const { loading } = useQuery<Data, Variables>(query, { variables });
583+
if (!loading) {
584+
finished = true;
585+
}
586+
return null;
587+
}
588+
589+
const mocksDifferentQuery = [
590+
{
591+
request: {
592+
query: gql`
593+
query OtherQuery {
594+
otherQuery {
595+
id
596+
}
597+
}
598+
`,
599+
variables
600+
},
601+
result: { data: { user } }
602+
}
603+
];
604+
605+
const link = new MockLink(
606+
mocksDifferentQuery,
607+
false,
608+
{ showWarnings: false }
609+
);
610+
611+
render(
612+
<MockedProvider link={link}>
613+
<Component {...variables} />
614+
</MockedProvider>
615+
);
616+
617+
await waitFor(() => {
618+
expect(finished).toBe(true);
619+
});
620+
621+
expect(console.warn).not.toHaveBeenCalled();
622+
623+
consoleSpy.mockRestore();
624+
});
625+
488626
itAsync('should support custom error handling using setOnError', (resolve, reject) => {
489627
let finished = false;
490628
function Component({ ...variables }: Variables) {
491629
useQuery<Data, Variables>(query, { variables });
492630
return null;
493631
}
494632

495-
const mockLink = new MockLink([]);
633+
const mockLink = new MockLink([], true, { showWarnings: false });
496634
mockLink.setOnError(error => {
497635
expect(error).toMatchSnapshot();
498636
finished = true;
@@ -521,7 +659,7 @@ describe('General use', () => {
521659
return null;
522660
}
523661

524-
const mockLink = new MockLink([]);
662+
const mockLink = new MockLink([], true, { showWarnings: false });
525663
mockLink.setOnError(() => {
526664
throw new Error('oh no!');
527665
});

0 commit comments

Comments
 (0)
Please sign in to comment.