2

I have the following ErrorLink set for Apollo Client.

export const errorLink = onError(
  ({ response, graphQLErrors, networkError, operation }: ErrorResponse) => {
    notificationService.notify("An Error Occurred");
  },
);

I need to test this implementation in a unit test. I've the following to test Apollo Links

const MockQuery = gql`
  query {
    foo
  }
`;

interface LinkResult<T> {
  operation: Operation;
  result: FetchResult<T>;
}

async function executeLink<T = ApolloLink>(
  linkToTest: ApolloLink,
  request: GraphQLRequest = { query: MockQuery },
) {
  const linkResult = {} as LinkResult<T>;

  return new Promise<LinkResult<T>>((resolve, reject) => {
    execute(ApolloLink.from([linkToTest]), request).subscribe(
      (result) => {
        linkResult.result = result as FetchResult<T>;
      },
      (error) => {
        reject(error);
      },
      () => {
        resolve(linkResult);
      },
    );
  });
}

it('triggers a notification on error', () => {
  const testLink = new ApolloLink(() => {
    await waitFor(() => expect(notificationSpy).toBeCalledWith('An Error Occurred'))

    return null;
  });

  const link = ApolloLink.from([errorLink, testLink]);
  executeLink(link);
});

These unit test work fine for other links like AuthLink where I test whether the auth token was set to the localStorage. But I cannot test the error link because I cannot trigger a GraphQL error.

Dave Howson
  • 156
  • 6
  • 14

2 Answers2

4

You can create a mocked terminating link and provide a GraphQL operation result.

E.g.

errorLink.ts:

import { onError } from '@apollo/client/link/error';

type ErrorResponse = any;

export const errorLink = onError(({ response, graphQLErrors, networkError, operation }: ErrorResponse) => {
  console.log('An Error Occurred');
  console.log('graphQLErrors: ', graphQLErrors);
});

errorLink.test.ts:

import { ApolloLink, execute, Observable } from '@apollo/client';
import { gql } from 'apollo-server-express';
import { errorLink } from './errorLink';

const MockQuery = gql`
  query {
    foo
  }
`;

describe('68629868', () => {
  test('should pass', (done) => {
    expect.assertions(1);
    const mockLink = new ApolloLink((operation) =>
      Observable.of({
        errors: [
          {
            message: 'resolver blew up',
          },
        ],
      } as any),
    );

    const link = errorLink.concat(mockLink);
    execute(link, { query: MockQuery }).subscribe((result) => {
      expect(result.errors![0].message).toBe('resolver blew up');
      done();
    });
  });
});

test result:

 PASS   apollo-graphql-tutorial  src/stackoverflow/68629868/errorLink.test.ts (5.02s)
  68629868
    ✓ should pass (14ms)

  console.log src/stackoverflow/68629868/errorLink.ts:6
    An Error Occurred

  console.log src/stackoverflow/68629868/errorLink.ts:7
    graphQLErrors:  [ { message: 'resolver blew up' } ]

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        5.067s

package version: @apollo/client@3.3.20

Lin Du
  • 88,126
  • 95
  • 281
  • 483
0

I specifically needed to test handling NetworkError with TypeScript and it was a right pain to figure out, so here's how you can do it:

import {
  ApolloLink,
  execute,
  FetchResult,
  from,
  gql,
  GraphQLRequest,
  Observable,
  Operation,
} from '@apollo/client'
import { errorLink, notificationService } from './'

interface LinkResult<T> {
  operation: Operation
  result: FetchResult<T>
}

const MockQuery = gql`
  query {
    foo
  }
`

class NetworkError extends Error {
  bodyText
  statusCode
  result
  message
  response

  constructor(networkErrorProps, ...params) {
    super(...params)

    const {
      name,
      bodyText,
      statusCode,
      result,
      message,
      response,
    } = networkErrorProps

    this.name = name
    this.bodyText = bodyText
    this.statusCode = statusCode
    this.result = result
    this.message = message
    this.response = response
  }
}

describe('errorLink', () => {
  it('should handle error and send notification', async () => {
    const mockLink = new ApolloLink((operation, forward) => {
      let fetchResult: FetchResult = {
        errors: [], // put GraphQLErrors here
        data: null,
      }

      // Thanks https://stackoverflow.com/a/70936974/21217
      let linkResult = Observable.of(fetchResult).map(_ => {
        throw new NetworkError({
          name: 'ServerParseError',
          message: 'Unexpected token',
          response: {},
          bodyText: '<!DOCTYPE html><html><head></head><body>Error</body></html>',
          statusCode: 503,
          result: {},
        })
      })
      return linkResult
    })

    async function executeLink<T = any, U = any>(
      dataLink: ApolloLink
    ) {
      const linkResult = {} as LinkResult<T>

      return new Promise<LinkResult<T>>((resolve, reject) => {
        execute(from([errorLink, dataLink]), {
          query: MockQuery,
        }).subscribe(
          result => {
            // We don't care
          },
          error => {
            // We can resolve here to skip having a try / catch around the await below
            resolve(linkResult)
          },
        )
      })
    }

    const notificationSpy = jest.spyOn(notificationService, 'notify')
    await executeLink(mockLink)
    expect(notificationSpy).toHaveBeenCalledWith('An Error Occurred')
  })
})
dain
  • 6,475
  • 1
  • 38
  • 47