Papupata Documentation

Guide: testing APIs

Overview

Testing can be tricky. This guide provides some ideas on how you could test APIs implemented using papupata.

Table of contents

Before starting

The following is assumed to be present for the examples in this guide:

Older styles are still supported in later versions, just not preferred.
import {APIDeclaration} from 'papupata'
const API = new APIDeclaration()

const complexAPI = API.declarePostAPI('/update/:id', routeOptions)
  .params({id: String})
  .query({author: String, notifyWatchers: Boolean}})
  .optionalQuery({timestamp: String})
  .body<{ name: string}>()
  .response<{status: string }>()

The Basics

As papupata is typically used as a part of an express application you always have the option of using whatever tools you use to test the rest of your express application.

The key to doing this is setting up a base URL on the papupata APIDeclaration and then using the getURL method of the individual APIs to figure out what to call.

API.configure({baseURL: 'http://localhost:3000'})

const response = await request.post(api.getURL({}))
expect(request).toEqual({ok: true})

There are a few downsides to doing this, though. For one, you are not taking advantage of types present in the APIs, so typescript will not be able to warn you if you are sending invalid data to the API. Second, by having the whole express application present you are testing the whole server implementation, including middleware, even if you wanted to do unit testing.

Let's look into some other options

Just testing the implementation

For the purpose of unit testing, you'll probably want to test just the implementation of the API. Once implemented, the actual implementation can be accessed as api.implementation. You can call it directly from your tests, although you might find it inconvenient because you have to supply request and response to the function.

const response = await api.implementation(request, response)

In practice, you'll want a helper of some kind to deal with setting up the objects.

Having papupata call the implementation

You can configure your tests to act as the papupata client, allowing you to use the normal papupata calls to test your API. The big advantage is having the syntax be exactly what the real client would use. There is a potential downside for clarity though: as this approach is focused on unit testing, you might want to reserve this syntax for making API calls that go through the full middleware stack. If that is the case, consider other options.

So, let's configure a client. We are using an experimental adapter for the job; it provides only a very minimal request and response as well as error handling. If it is too limited for your needs, feel free to use the code for the adapter to build your own.

import createAdapter from 'papupata/adapters/invokeImplementation'
API.updateConfig({
  baseURL: '', // the value is not relevant, but must be a string
  requestAdapter: createAdapter()
})
const response = await api({id: '1', author: 'Sinead', notifyWatchers: false, name: 'Ulrich'})

The adapters supports a few options that make your life easier:

  • createRequest for setting up the request as you need (with, say, headers, session etc)
    const requestAdapter = createAdapter({
      createRequest: () => ({ headers: { 'authorization': 'bearer 123'}})
    })
    
  • assertResponse for making assertions about the response beyond just the data
    const requestAdapter = createAdapter({
      assertResponse: res => expect(res.statusCode).toEqual(400)
    })
    
  • withMiddleware, which enables the use of middleware for the request
    const requestAdapter = createAdapter({
      withMiddleware: true
    })
    
Call the implementation with a helper function

With properly designed helper function you can call the implementation indirectly with full type safety. Papupata comes with an experimental function that might or might not be good enough for your use case. If you need additional features, feel free to use the provided function as a template to work on.

import testInvoke from 'papupata/invokers/test'
const response = await invoker(api, {id: '1', author: 'Sinead', notifyWatchers: false, name: 'Ulrich'})

The test invoker supports the same options as invokeImplementationAdapter described above.

const response = await invoker(api, data, { withMiddleware: true })

Full stack tests

For full stack tests you'd be best off setting up papupata to make the calls as if it's the real client.

If you are setting up the express server with your own code for your tests, you might want to use the request-promise adapter or write your own if you use something else.

import createRequestAdapter from 'papupata/adapters/requestPromise'
API.updateConfig({
  baseURL: `http://localhost:${port}`
  requestAdapter: createRequestAdapter('json')
})
const response = await api({id: '1', author: 'Sinead', notifyWatchers: false, name: 'Ulrich'})

If you are using supertest, you can use adapter specifically made for it instead.

import createSupertestAdapter from 'papupata/adapters/supertest'

const supertestRequest = supertest(app) // express app
API.updateConfig({
  baseURL: '', // The value must be an empty string
  requestAdapter: createSupertestAdapter(supertestRequest)
})
const response = await api({id: '1', author: 'Sinead', notifyWatchers: false, name: 'Ulrich'})

If you wish to access the actual supertest request for your assertions, you can instead use supertest invoker.

import supertestInvoker from 'papupata/invokers/supertest'

API.updateConfig({
  baseURL: '', // The value must be an empty string
})

const supertestRequest = supertest(app) // express app
const data = {id: '1', author: 'Sinead', notifyWatchers: false, name: 'Ulrich'}

await invokeSupertest(supertestRequest, api, data)
  .expect(200)