Last active
March 10, 2025 19:28
-
-
Save LionelB5/ab354f856f898c060d88edd29353597e to your computer and use it in GitHub Desktop.
Apollo Client V3 link that supports mocking GraphQL subscriptions, mutations and queries. Particularly useful when testing components that rely on queries/mutations that execute concurrently to subscriptions that must be mocked.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import type { FetchResult, Operation } from "@apollo/client"; | |
import { Observable } from "@apollo/client"; | |
import { MockLink } from "@apollo/client/testing"; | |
import type { Observer } from "@apollo/client/utilities"; | |
// Apollo Client's out of the box utilities for testing subscriptions are limited: | |
// - The `mocks` argument provided to Apollo's `MockedProvider` is only capable of mocking a | |
// single subscription payload per subscription. | |
// - Apollo's `MockSubscriptionLink` can be used to mock multiple subscription payloads via | |
// a `simulateResult` method. However, the `simulateResult` method delivers data to _every_ | |
// active query, making it unsuitable for testing components that rely on multiple different | |
// GraphQL operations. | |
// | |
// This mock extends Apollo's `MockLink` (the one used behind the scenes by `MockedProvider`) | |
// and adds a `simulateSubscriptionResult` method that can be used to simulate incoming | |
// subscription data. | |
// - Unlike `MockSubscriptionLink`, `simulateSubscriptionResult` allows for delivering data to a | |
// specific query by supplying an `operationName`. Data is delievered to all active subscriptions | |
// with the given `operationName`, a limitation of the current implementation is that it does not | |
// discriminate operations using their variables, only the operation name is used, as a result | |
// subscriptions with different variables will both receive data if they are for the same operation. | |
// - All non-subscription operations fallback to Apollo's `MockLink` implementation. | |
// Non-subscription operations can be mocked by providing `mocks` to the link's constructor. | |
// | |
// The `simulateSubscriptionResult` method takes inspiration from the `simulateResult` method of | |
// Apollo's `MockSubscriptionLink`. https://tinyurl.com/3yfkbr7d | |
class MockedLink extends MockLink { | |
private subscriptionObserverMapping: Record<string, Observer<FetchResult>> = {}; | |
public request(operation: Operation): ReturnType<MockLink["request"]> { | |
if (isSubscription(operation)) { | |
return new Observable<FetchResult>((observer) => { | |
this.subscriptionObserverMapping[operation.operationName] = observer; | |
}); | |
} else { | |
return super.request(operation); | |
} | |
} | |
public simulateSubscriptionResult({ | |
result, | |
error, | |
operationName, | |
delay = 0, | |
}: { | |
result?: FetchResult; | |
error?: Error; | |
operationName: string; | |
delay?: number; | |
}): void { | |
setTimeout(() => { | |
const observer = this.subscriptionObserverMapping[operationName]; | |
if (!observer) { | |
throw new Error(`subscription for operation ${operationName} not found`); | |
} | |
if (result && observer.next) { | |
observer.next(result); | |
} | |
if (error && observer.error) { | |
observer.error(error); | |
} | |
}, delay); | |
} | |
} | |
const isSubscription = (operation: Operation): boolean => { | |
return operation.query.definitions.some( | |
(d) => d.kind === "OperationDefinition" && d.operation === "subscription", | |
); | |
}; | |
export default MockedLink; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { MockedProvider, MockedResponse } from "@apollo/client/testing"; | |
import { act } from "@testing-library/react"; | |
import { FOO_QUERY } from "../queries"; | |
import MockedLink from "../MockedLink"; | |
describe("example", () => { | |
test("example usage", async () => { | |
// simulate requests or mutations | |
const mocks: MockedResponse[] = [ | |
{ request: { query: FOO_QUERY }, result: { data: { foo: "FOO_DATA" } } }, | |
]; | |
const link = new MockedLink(mocks); | |
render( | |
<MockedProvider link={link}> | |
<ComponentUnderTest /> | |
</MockedProvider>, | |
); | |
// simulate subscription operation receiving data | |
await act(() => { | |
link.simulateSubscriptionResult({ | |
result: { data: { onBar: "barData" } }, | |
operationName: "BarSubscription", | |
}); | |
}); | |
}); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment