Apollo Client is my go-to GraphQL client implementation because it’s so fully-featured. For the most part, this one library covers my state management, network connections, and can even “decorate” data like dates & URLs so I can use more sophisticated tooling for dealing with them.
There’s one major flaw that I’d like to address, though. Apollo’s testing story is confusing at best and actively harmful to applications at worst. I know this is turning into a “…considered harmful” post, but I’m serious: I’ve never used a mocking library that let you shoot yourself in the foot quite as much as @apollo/client/testing
, to the point where I actually check $NODE_ENV === "test"
in the implementation just to get around its bullshit so I can actually test what I want to test. Whenever you’re using $NODE_ENV
to bypass actual implementation code (which can break!) for the sake of your test framework, something is extremely wrong.
Mocking
In order to test a single component in my application, I need a mocks
array that is over 300 lines long. Most of this code is totally meaningless, it’s just there to satisfy the “loading state” because otherwise Apollo throws errors since it can’t find any more mocks.
Here’s an example of what that looks like:
{
request: {
query: CommunityNameDocument,
variables: { id },
},
result: {
data: {
community: {
name: 'Community Name',
},
},
},
},
{
request: {
query: KickedFromRoomDocument,
variables: { room },
},
result: {
data: {},
},
},
{
request: {
query: KickedFromCommunityDocument,
variables: { community: id },
},
result: {
data: {},
},
},
{
request: {
query: KickedFromCommunityDocument,
variables: { community: id },
},
result: {
data: {},
},
},
{
request: {
query: NewMessageDocument,
variables: {},
},
result: {
data: {},
},
},
{
request: {
query: NewMessageDocument,
variables: { room },
},
result: {
data: {},
},
},
{
request: {
query: AddStreamToRoomDocument,
variables: { id: room, url: 'https://twitch.tv/nerdstreet' },
},
result: {
data: {
addStreamToRoom: {
stream: {
channel: 'nerdstreet',
},
},
},
},
},
Notice how much duplication there is? Those KickedFrom*Document
operations are subscriptions, and in all honesty I’m not testing their functionality at all in this component, yet I still need hundreds of lines of bullshit code just to satisfy Apollo’s weird requirements. In this particular instance, I feel that MockedProvider
should give me the option to disable subscriptions. If I’m not testing subscriptions, all they do is get in the way.
Success vs Loading State
Another huge issue that really threw me for a loop in the beginning (and is probably contributing to the above problem!) is that the “loading” state is actually the default out-of-box state that occurs in the test. This means that newcomers who didn’t read the one line in the docs that happens to specify this (buried WAY DEEP in the middle of the page too!) will be completely confused as to why their test framework doesn’t work.
Also, this line? Doesn’t do anything.
await new Promise((resolve) => setTimeout(resolve, 0))
…except give me a bunch of cryptic errors in my tests.
I think this is actually the biggest problem with Apollo Testing, and changing render()
to return a promise and/or making the success state the default behavior is (in my opinion) the most significant change that can be made in order to improve the testing experience. When you sit down and write tests, you’re trying to express what happens when something occurs in the app, and hopefully from the perspective of a user. I don’t want to have to think about Apollo’s implementation details to write tests for my application, otherwise the testing framework is just useless.
The Cache
The cache is on by default in tests. Why?
This is how I have to set up a MockedProvider in order for things to actually work:
<MockedProvider
mocks={mocks}
addTypename={false}
defaultOptions={{ watchQuery: { fetchPolicy: 'no-cache' } }}
>
I just find all of this to be incredibly confusing, and it’s no wonder why people are afraid to test Apollo apps. Testing doesn’t even work without the last two props most of the time, so why can’t those just be default?