Skip to content

Commit

Permalink
Log unmatched mocks to the console for tests (#10502)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
jerelmiller authored Feb 3, 2023
1 parent 14a56b1 commit 315faf9
Show file tree
Hide file tree
Showing 8 changed files with 218 additions and 18 deletions.
5 changes: 5 additions & 0 deletions .changeset/funny-files-suffer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@apollo/client': patch
---

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.
16 changes: 16 additions & 0 deletions docs/source/api/react/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,22 @@ Props to pass down to the `MockedProvider`'s child.
</td>
</tr>

<tr>
<td>

###### `showWarnings`

`boolean`
</td>
<td>

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.

The default value is `true`.

</td>
</tr>

</tbody>
</table>

Expand Down
13 changes: 10 additions & 3 deletions docs/source/development-testing/testing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -394,12 +394,19 @@ In order to properly test local state using `MockedProvider`, you'll need to pas
`MockedProvider` creates its own ApolloClient instance behind the scenes like this:

```jsx
const { mocks, addTypename, defaultOptions, cache, resolvers, link } =
this.props;
const {
mocks,
addTypename,
defaultOptions,
cache,
resolvers,
link,
showWarnings,
} = this.props;
const client = new ApolloClient({
cache: cache || new Cache({ addTypename }),
defaultOptions,
link: link || new MockLink(mocks || [], addTypename),
link: link || new MockLink(mocks || [], addTypename, { showWarnings }),
resolvers,
});
```
Expand Down
28 changes: 21 additions & 7 deletions src/core/__tests__/ObservableQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { ApolloLink } from '../../link/core';
import { InMemoryCache, NormalizedCacheObject } from '../../cache';
import { ApolloError } from '../../errors';

import { itAsync, mockSingleLink, subscribeAndCount } from '../../testing';
import { itAsync, MockLink, mockSingleLink, subscribeAndCount } from '../../testing';
import mockQueryManager from '../../testing/core/mocking/mockQueryManager';
import mockWatchQuery from '../../testing/core/mocking/mockWatchQuery';
import wrap from '../../testing/core/wrap';
Expand Down Expand Up @@ -1509,11 +1509,25 @@ describe('ObservableQuery', () => {
};
}

const observableWithVarsVar: ObservableQuery<any> = mockWatchQuery(
reject,
makeMock("a", "b", "c"),
makeMock("d", "e"),
);
// We construct the queryManager manually here rather than using
// `mockWatchQuery` because we need to silence console warnings for
// unmatched variables since. This test checks for calls to
// `console.warn` and unfortunately `mockSingleLink` (used by
// `mockWatchQuery`) does not support the ability to disable warnings
// without introducing a breaking change. Instead we construct this
// manually to be able to turn off warnings for this test.
const mocks = [makeMock('a', 'b', 'c'), makeMock('d', 'e')];
const firstRequest = mocks[0].request;
const queryManager = new QueryManager({
cache: new InMemoryCache({ addTypename: false }),
link: new MockLink(mocks, true, { showWarnings: false })
})

const observableWithVarsVar = queryManager.watchQuery({
query: firstRequest.query,
variables: firstRequest.variables,
notifyOnNetworkStatusChange: false
});

subscribeAndCount(error => {
expect(error.message).toMatch(
Expand All @@ -1536,7 +1550,7 @@ describe('ObservableQuery', () => {
// to call refetch(variables).
observableWithVarsVar.refetch({
variables: { vars: ["d", "e"] },
}).then(result => {
} as any).then(result => {
reject(`unexpected result ${JSON.stringify(result)}; should have thrown`);
}, error => {
expect(error.message).toMatch(
Expand Down
1 change: 1 addition & 0 deletions src/testing/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export {
MockLink,
mockSingleLink,
MockedResponse,
MockLinkOptions,
ResultFunction
} from './mocking/mockLink';
export {
Expand Down
18 changes: 17 additions & 1 deletion src/testing/core/mocking/mockLink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ export interface MockedResponse<TData = Record<string, any>> {
newData?: ResultFunction<FetchResult>;
}

export interface MockLinkOptions {
showWarnings?: boolean;
}

function requestToKey(request: GraphQLRequest, addTypename: Boolean): string {
const queryString =
request.query &&
Expand All @@ -40,14 +44,18 @@ function requestToKey(request: GraphQLRequest, addTypename: Boolean): string {
export class MockLink extends ApolloLink {
public operation: Operation;
public addTypename: Boolean = true;
public showWarnings: boolean = true;
private mockedResponsesByKey: { [key: string]: MockedResponse[] } = {};

constructor(
mockedResponses: ReadonlyArray<MockedResponse>,
addTypename: Boolean = true
addTypename: Boolean = true,
options: MockLinkOptions = Object.create(null)
) {
super();
this.addTypename = addTypename;
this.showWarnings = options.showWarnings ?? true;

if (mockedResponses) {
mockedResponses.forEach(mockedResponse => {
this.addMockedResponse(mockedResponse);
Expand Down Expand Up @@ -102,6 +110,14 @@ Failed to match ${unmatchedVars.length} mock${
} for this query. The mocked response had the following variables:
${unmatchedVars.map(d => ` ${stringifyForDisplay(d)}`).join('\n')}
` : ""}`);

if (this.showWarnings) {
console.warn(
configError.message +
'\nThis typically indicates a configuration error in your mocks ' +
'setup, usually due to a typo or mismatched variable.'
);
}
} else {
mockedResponses.splice(responseIndex, 1);

Expand Down
5 changes: 4 additions & 1 deletion src/testing/react/MockedProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export interface MockedProviderProps<TSerializedCache = {}> {
childProps?: object;
children?: any;
link?: ApolloLink;
showWarnings?: boolean;
}

export interface MockedProviderState {
Expand All @@ -40,14 +41,16 @@ export class MockedProvider extends React.Component<
defaultOptions,
cache,
resolvers,
link
link,
showWarnings,
} = this.props;
const client = new ApolloClient({
cache: cache || new Cache({ addTypename }),
defaultOptions,
link: link || new MockLink(
mocks || [],
addTypename,
{ showWarnings }
),
resolvers,
});
Expand Down
150 changes: 144 additions & 6 deletions src/testing/react/__tests__/MockedProvider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ describe('General use', () => {
};

render(
<MockedProvider mocks={mocks}>
<MockedProvider showWarnings={false} mocks={mocks}>
<Component {...variables2} />
</MockedProvider>
);
Expand Down Expand Up @@ -215,7 +215,7 @@ describe('General use', () => {
};

render(
<MockedProvider mocks={mocks2}>
<MockedProvider showWarnings={false} mocks={mocks2}>
<Component {...variables2} />
</MockedProvider>
);
Expand Down Expand Up @@ -325,7 +325,7 @@ describe('General use', () => {
];

render(
<MockedProvider mocks={mocksDifferentQuery}>
<MockedProvider showWarnings={false} mocks={mocksDifferentQuery}>
<Component {...variables} />
</MockedProvider>
);
Expand Down Expand Up @@ -435,7 +435,10 @@ describe('General use', () => {
return null;
}

const link = ApolloLink.from([errorLink, new MockLink([])]);
const link = ApolloLink.from([
errorLink,
new MockLink([], true, { showWarnings: false })
]);

render(
<MockedProvider link={link}>
Expand Down Expand Up @@ -485,14 +488,149 @@ describe('General use', () => {
expect(errorThrown).toBeFalsy();
});

it('shows a warning in the console when there is no matched mock', async () => {
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
let finished = false;
function Component({ ...variables }: Variables) {
const { loading } = useQuery<Data, Variables>(query, { variables });
if (!loading) {
finished = true;
}
return null;
}

const mocksDifferentQuery = [
{
request: {
query: gql`
query OtherQuery {
otherQuery {
id
}
}
`,
variables
},
result: { data: { user } }
}
];

render(
<MockedProvider mocks={mocksDifferentQuery}>
<Component {...variables} />
</MockedProvider>
);

await waitFor(() => {
expect(finished).toBe(true);
});

expect(console.warn).toHaveBeenCalledTimes(1);
expect(console.warn).toHaveBeenCalledWith(
expect.stringContaining('No more mocked responses for the query')
);

consoleSpy.mockRestore();
});

it('silences console warning for unmatched mocks when `showWarnings` is `false`', async () => {
const consoleSpy = jest.spyOn(console, 'warn');
let finished = false;
function Component({ ...variables }: Variables) {
const { loading } = useQuery<Data, Variables>(query, { variables });
if (!loading) {
finished = true;
}
return null;
}

const mocksDifferentQuery = [
{
request: {
query: gql`
query OtherQuery {
otherQuery {
id
}
}
`,
variables
},
result: { data: { user } }
}
];

render(
<MockedProvider mocks={mocksDifferentQuery} showWarnings={false}>
<Component {...variables} />
</MockedProvider>
);

await waitFor(() => {
expect(finished).toBe(true);
});

expect(console.warn).not.toHaveBeenCalled();

consoleSpy.mockRestore();
});

it('silences console warning for unmatched mocks when passing `showWarnings` to `MockLink` directly', async () => {
const consoleSpy = jest.spyOn(console, 'warn');
let finished = false;
function Component({ ...variables }: Variables) {
const { loading } = useQuery<Data, Variables>(query, { variables });
if (!loading) {
finished = true;
}
return null;
}

const mocksDifferentQuery = [
{
request: {
query: gql`
query OtherQuery {
otherQuery {
id
}
}
`,
variables
},
result: { data: { user } }
}
];

const link = new MockLink(
mocksDifferentQuery,
false,
{ showWarnings: false }
);

render(
<MockedProvider link={link}>
<Component {...variables} />
</MockedProvider>
);

await waitFor(() => {
expect(finished).toBe(true);
});

expect(console.warn).not.toHaveBeenCalled();

consoleSpy.mockRestore();
});

itAsync('should support custom error handling using setOnError', (resolve, reject) => {
let finished = false;
function Component({ ...variables }: Variables) {
useQuery<Data, Variables>(query, { variables });
return null;
}

const mockLink = new MockLink([]);
const mockLink = new MockLink([], true, { showWarnings: false });
mockLink.setOnError(error => {
expect(error).toMatchSnapshot();
finished = true;
Expand Down Expand Up @@ -521,7 +659,7 @@ describe('General use', () => {
return null;
}

const mockLink = new MockLink([]);
const mockLink = new MockLink([], true, { showWarnings: false });
mockLink.setOnError(() => {
throw new Error('oh no!');
});
Expand Down

0 comments on commit 315faf9

Please sign in to comment.