1.4.1 • Published 5 months ago

@sr1ch1/apollo-fetcher-stub v1.4.1

Weekly downloads
-
License
MIT
Repository
github
Last release
5 months ago

GitHub Workflow Status Coverage Status npm GitHub

apollo-fetcher-stub

A stub version of the Apollo Server's fetcher interface to simplify integration testing of resolvers that are based on Apollo Server's RESTDataSource.

Introduction

This package provides a stub implementation of the fetcher interface that is useful when creating integration tests for resolvers in the Apollo Server 4. Requests along with corresponding responses can be specified through the fluent interface of this library.

Overview

Apollo Server 4 uses a multi-step request pipeline to execute incoming GraphQL requests. It allows using plugins for each step. The plugins may affect the outcome of a GraphQL operation. This makes it important to test the server with plugins enabled.

To enforce separation of concerns and simplify writing integration tests the use of following pattern is recommended when fetching data from REST-based services:

apollo-fetcher-stub.svg

A recommended pattern for implementing resolvers is to use data sources to retrieve/modify data. As it is a common use case to fetch data from REST-based services Apollo Server comes with a RESTDataSource class that can be used as a base class for specific data source implementation. The RESTDataSource makes use of a specific fetcher interface in order to perform HTTP requests. The fetcher interface can be used to plug in different fetch libraries (node-fetch, make-fetch-happen, or undici).

This library uses the same interface to inject a stub fetcher that can be instrumented for integration testing.

Installation

Requirements

To install the package use:

npm install --save-dev sr1ch1/apollo-fetcher-stub

or with yarn:

yarn add -D sr1ch1/apollo-fetcher-stub

Usage

As this library is intended to be used as a fetcher replacement the documentation will focus on the described pattern above. It is not are requirement to rely on the RESTDataSource class. The fetcher stub can be used in any context where Apollo's fetcher interface is used.

Premise

To give a better context in the use of the fetcher stub, it is assumed that you have implemented a data source based on the RESTDataSource

// this is the implementation of a specific data source based on Apollo Server's RESTDataSource
export class ExampleAPI extends RestDataSource {
  // ... implementation of getData as an example of a function to be made available
  // if this contains nontrivial transformations, you may also want to test this.
}

Furthermore, you have a resolver that uses this data source

export const exampleResolver = {
  Query: {
    populations: async (_, __, {dataSources}): Promise<void>  => {
      // fetch data via data source. Here we are using the exampleAPI, 
      // that is injected as a data source.
      const result = await dataSources.exampleAPI.getData()
      // ... the logic you want to test along with the pipeline logic you might have in place.
    }
  },
};

Setting up an integration Test

To create an integration test, we need the following steps: 1. Create and set up the fetcher stub. It must respond to the expected requests with appropriate data. This step is specific for each integration test. In this example the fetcher stub will return the status code 204 whenever it receives a get request with the URL http://localhost:8080/ping

// import the stub
import {FetcherStub} from "@sr1ch1/apollo-fetcher-stub";

// first create a new stub instance
const fetcherStub = new FetcherStub()

// when the fetcher receives a specific get request
fetcherStub.get("http://localhost:8080/ping")
// it responds with this status code
.responds().withStatusCode(204);
  1. Create an Apollo context function that makes use of the fetcher stub.
const createContext = async (): Promise<ContextValue> => {
  return {
    dataSources: {
      // here we create the data source and injecting it with the fetcher stub
      example: new ExampleAPI({ fetcher: fetcherStub }),
    }
  };
}
  1. Create an Apollo test server that is using a configuration close to your production server.
// For the integration tests, create a test server that is using
// the prepared context for the text
const testServer = new ApolloServer<ContextValue>({
  typeDefs, // the real schema 
  resolvers,// the real resolvers
  plugins   // the real plugins
});
  1. Run the desired GraphQL query on the test server using the prepared context.
// create the context with the stub fetcher and real rest data sources.
const contextValue = await createContext(); 

// execute the test GraphQL query using the test server with the real configuration
// the only thing that is stubbed is the fetcher the RESTDataSource is using.
// This ensures we test as much of the Apollo Stack as possible.
const response = await testServer.executeOperation({ query }, { contextValue });
  1. the response is a GraphQLResponse object and can be tested by querying the available properties.
// in this example it is assumed we receive a single data set and
// directly test the properties (example with Jest)
expect(response?.body?.kind).toBe('single');
expect(response.body['singleResult']).toBeDefined();
expect(response.body['singleResult']['data']).toBeDefined();
const myData = response.body['singleResult']['data']['myData'];
// test the properties of myData ...

Fluent interface of the fetcher stub

The fetcher stub has a fluent interface that allows you to create a stub in a declarative and readable way. This helps keeping the integration tests clean. The interface is modelled after the structure of an HTTP request. It does not provide any kind of validation though, to be open for writing tests with invalid requests. The following Http methods are available:

const fetcherStub = new FetcherStub()
const URL = 'http://localhost:4000/some-path'

fetcherStub.get(URL);
fetcherStub.head(URL);
fetcherStub.post(URL);
fetcherStub.put(URL);
fetcherStub.patch(URL);
fetcherStub.delete(URL);
fetcherStub.options(URL);

After specifying the HTTP method, you can add any header you need:

fetcherStub.get(URL)
  .withHeader('Accept', 'application/json')
  .withHeader('User-Agent', 'Mozilla/5.0 (platform; rv:geckoversion) Gecko/geckotrail Firefox/firefoxversion');

For POST or PATCH requests, you can also add a body like this:

fetcherStub.get(URL)
  .withHeader('User-Agent', 'Mozilla/5.0 (platform; rv:geckoversion) Gecko/geckotrail Firefox/firefoxversion');
  .withBody('{name:"Max"}', 'application/json')

The second parameter, the content type is optional. You could also use the withHeader function to specify a content type.

After specifying the request, we need to configure what the response would be. This can be as simple as returning a status code:

fetcherStub.get(URL)
  .responds()
  .withStatusCode(200);

// or alternatively
fetcherStub.get(URL)
  .responds()
  .withStatusCode(201)
  .withStatusText('Ok');

// or
fetcherStub.get(URL)
  .responds()
  .withStatus(201, 'Ok');

But if response headers and body are needed this can be done like this:

fetcherStub.get(URL)
  .responds()
  .withStatusCode(200)
  .withHeader('Server', 'Some Server')
  .withBody('{"name":"Max", "age":33}', 'application/json')

The second parameter, the content type is also optional and if desired the content type can be specified directly as a header.

With these functions even more complex scenarios can be handled. The fetcher stub collects the specification and uses it during the integration test. You could specify multiple different requests should that be necessary. Complex scenarios can be covered this way.

Here is an example how to stub a preflight request (CORS scenario):

fetcherStub.options("/")
  .withHeader('Host', 'service.example.com')
  .withHeader('Origin', 'https://www.example.com')
  .withHeader('Access-Control-Request-Method', 'PUT')
  .responds()
  .withHeader('Access-Control-Allow-Origin', 'https://www.example.com')
  .withHeader('Access-Control-Allow-Methods', 'PUT')
  .withStatusCode(200)
  .withStatusText('Ok');

By adding multiple requests, it is also possible to handle OAUTH scenarios. This is useful when you want to test login scenarios in a fast and reliable way.

Sometimes it is not good enough to provide a static URL that must be stubbed. This can be the case when you cannot complete control the URL to test with. For example, when the resolver uses random values, time dependent values or other values that you cannot influence. In this special case you can pass a regular expression instead of a string as URL. As an example we could have a scenario where the uncontrollable piece of data is a query parameter named code. We only know the structure of the code not the exact value. We can create a regular expression for it and use it as parameter. Here it is assumed that the code consists of capital letters and digits and is exactly 8 characters long.

const fetcherStub = new FetcherStub()
const URL = /http:\/\/localhost:4000\/some-path?code=[A-Z0-9]{8}/

fetcherStub.get(URL);
1.4.1

5 months ago

1.3.0

11 months ago

1.2.0

1 year ago

1.1.0

1 year ago

1.0.0

1 year ago