Real world testing recipes: Node service that calls an external API
Tags: javascript, node, testing, unittesting
Published: 2018-11-26 01:41:25

---
title: Real world testing recipes: Node service that calls an external API
published: true
tags: javascript,nodejs,testing,unittesting
canonical_url: https://www.coreycleary.me/real-world-testing-recipes-node-service-that-calls-an-external-api/
cover_image: https://thepracticaldev.s3.amazonaws.com/i/4lvurv08krqdyl8pu86i.png
series: Real World Testing Recipes
---
<em><strong>Originally published at <a href="https://www.coreycleary.me/real-world-testing-recipes-node-service-that-calls-an-external-api/">coreycleary.me</a></strong>. This is a cross-post from my content blog. I publish new content every week or two, and you can <a href="https://www.coreycleary.me/about/">sign up to my newsletter</a> if you'd like to receive my articles directly to your inbox! I also regularly send cheatsheets, links to other great tutorials (by other people), and other freebies.</em>

This is the next post in my series on knowing what to test in your Node applications by applying <strong>recipes</strong> to <strong>real-world scenarios</strong>.

In the first one, we went over <a href="https://www.coreycleary.me/know-what-to-test-using-these-recipes-node-service-that-calls-a-database/">scenarios you should cover in your tests when calling a database from a Node service</a>.

In this post, we'll cover another very common real-world application: a Node service that calls an external REST API/endpoint. "External" means it is an application outside of our own - think the Twitter REST API, Google Maps API, or even an API internal to your company, but not part of your application.

<h3>In case you're new to the series...</h3>

If you've ever gone through a JavaScript or Node unit testing tutorial, it can be difficult to find anything that shows you <strong><em>what</em></strong> you should be testing - <strong><em>what scenarios</em></strong> to cover, <strong><em>what</em></strong> to write tests for, etc.

It's easy to find tutorials that take you through very basic examples - things like how to write tests for addition functions or tests for checking object properties - but more difficult to find ones that go beyond the basics as well as cover real-world scenarios.

As a developer you know you "should" be writing tests if you want to be considered a "good developer". But if you don't know the kinds of test scenarios you should be looking out for, it can be hard to write them in the first place. And if you're completely new to writing tests, it's even more frustrating.

When you've got a feature you need to implement at work, deadlines are looming, and you're stuck when it comes to the tests, usually those tests don't get written at all.

<h3>Application of testing recipes</h3>

When I was learning how to write tests for my code, I faced this frustration too. But I learned what things to cover by a couple different means:
<ul>
	<li>getting bug reports for scenarios my tests should have covered</li>
	<li>reading through lots of tests for existing codebases, both work codebases as well as open source ones</li>
</ul>

I eventually noticed that a lot of tests covered a similar set of scenarios. Depending on what part of an application you're working on, there are things to look out for to make sure your tests - and by extension, code - cover so you can be sure any new changes introduced into the application later on will catch anything if it breaks.

These scenarios are distilled from what I've found. You can use these as a starting point for when you encounter a similar application.

Ultimately, not only will you know what tests to write, <a href="https://www.coreycleary.me/tried-tdd-and-didnt-realize-the-benefits-try-it-the-next-time-you-get-writers-block/" rel="noopener" target="_blank">the tests will also help inform the design/implementation of your code</a>.

<h3>Our real-world example</h3>
We'll be writing tests for a library application that allows you to search for library books by title.

The API we will be interacting with will be the <a href="https://openlibrary.org/dev/docs/api/search" rel="noopener" target="_blank">Open Library Search API</a>.

The complete code (with tests!) can be <a href="https://github.com/coreyc/test-recipes-service-to-api" rel="noopener" target="_blank">downloaded here</a>, but I recommend following along here first. After all, the point of this post is to help you identify <em>scenarios</em> to cover rather than just understand the code.

And with that, let's start to get into our recipes...

<h3>Scenario 1: Does our Node service call the external API successfully?</h3>
Here's our initial code to actually call the API. In our Node service - `book.service.js`:
```javascript
const request = require('superagent')

const fetchBooks = async (query) => {
  return await request
      .get('http://openlibrary.org/search.json')
      .query({ q: query }) // query string
}

module.exports = {
  fetchBooks
}
```
So what scenario should the test cover?
```javascript
const chai = require('chai')
const chaiAsPromised = require('chai-as-promised')
const nock = require('nock')

const { booksFixture } = require('./books.fixture')

const { fetchBooks } = require('../src/book-service')

const expect = chai.expect
chai.use(chaiAsPromised)

describe('Book Service', () => {
  describe('fetchBooks', () => {
    it('should return list of books based on search string', async () => {
      nock('http://openlibrary.org')
        .get('/search.json')
        .query(true)
        .reply(200, booksFixture)

      const {body} = await fetchBooks('lord of the rings')
      expect(body).to.deep.equal({
        docs: [
          {title_suggest: 'The Lord of the Rings', cover_edition_key: 'OL9701406M'},
          {title_suggest: 'Lord of the Rings', cover_edition_key: 'OL1532643M'},
          {title_suggest: 'The Fellowship of the Ring', cover_edition_key: 'OL18299598M'}
        ]
      })
    })
  })
})
```
What we test here is that we get back a successful response from the API (<strong>200 status code</strong>) and we get our book results back. Easy, right?

Because this is our first test we've added, so let's quickly go over <strong>how</strong> we're testing it.

<h3>Mocking</h3>
Making use of mocks in tests could be an entire book by itself, but here we use it in a pretty simple way with <a href="https://github.com/nock/nock" rel="noopener" target="_blank">the npm module, `nock`</a>.

`nock` will listen for calls made to the url we specify - in this case the Open Library REST API - and will "intercept" those calls. So instead of <em>actually</em> calling the <strong>real</strong> Open Library REST API, we specify the fake response to return. 

We do this because:
<ul>
	<li>Calling a real HTTP API introduces latency</li>
	<li>Latency slows our tests down</li>
	<li>We have less control over the data returned</li>
	<li>The data returned might be slightly different, which would break our tests</li>
</ul>
<h3>Fixtures</h3>
And the fake response we return? That is our <strong>fixture</strong>. Just like mocks, fixtures are too big a concept to cover entirely here. <em>But</em> ultimately they're pretty easy.

This is what the real response looks like from the Open Library REST API:
<img src="https://www.coreycleary.me/wp-content/uploads/2018/11/Screen-Shot-2018-11-21-at-12.31.59-PM.png" alt="" width="439" height="486" class="aligncenter size-full wp-image-822" />
Each of those properties like `isbn` and `text` are arrays with potentially hundreds of items. Can you imagine if we had to reproduce that response by hand? We could copy and paste the JSON response, but even then it would take up the whole test file and be really difficult to read.

Instead of reproducing the whole response, we only reproduce a subset of it. This gives us enough data to test what we need without having to clutter up our tests.

And that data goes in our `books.fixture.js` file:
```javascript
const booksFixture = {
  docs: [
    {title_suggest: 'The Lord of the Rings', cover_edition_key: 'OL9701406M'},
    {title_suggest: 'Lord of the Rings', cover_edition_key: 'OL1532643M'},
    {title_suggest: 'The Fellowship of the Ring', cover_edition_key: 'OL18299598M'}
  ]
}

module.exports = {
  booksFixture
}
```

<h3>Scenario 2: What if the external API is down?</h3>
That's awesome if the API is running functionally, our current code and tests cover that.

But what if the API is down? How will <strong>our</strong> code handle that?

Let's take care of it in our code first. In `book.service.js`, let's modify our `fetchBooks` function:
```javascript
const fetchBooks = async (query) => {
  let response
  
  try {
    response = await request
      .get('http://openlibrary.org/search.json')
      .query({ q: query }) // query string
  } catch(e) {
    response = e.status
  }

  if (response.status === 500) throw new Error('Open Library service down')
  else return response
}
```
Cool, let's add the test for that:
```javascript
    it('should throw an error if the service is down', async () => {
      nock('http://openlibrary.org')
        .get('/search.json')
        .query(true)
        .reply(500)

      await expect(fetchBooks('lord of the rings')).to.be.rejected
    })
```
I chose to throw an error here in our service, which the controller which calls this service would then have to catch and handle. But we could just as easily return null or an empty array. This more just depends on your requirements.

<h3>Scenario 3: What if the external API finds nothing for our query?</h3>
If the API is up, but our search returns nothing, we'll get a `404` response code from the API. So let's handle that too:
```javascript
const fetchBooks = async (query) => {
  let response
  
  try {
    response = await request
      .get('http://openlibrary.org/search.json')
      .query({ q: query }) // query string
  } catch(e) {
    response = e.status
  }

  if (response.status === 404) return null
  if (response.status === 500) throw new Error('Open Library service down')
  else return response
}
```
And the test:
```javascript
    it('should return null if query returns a 404', async () => {
      nock('http://openlibrary.org')
        .get('/search.json')
        .query(true)
        .reply(404)

      const response = await fetchBooks('aksdfhkahsdfkhsadkfjhskadjhf')
      expect(response).to.be.null;
    })
```
Easy!

<h3>Scenario 4: What if there is a problem with our request?</h3>
There are several things that could be wrong with our request:
<ul>
	<li>We could have accidentally forgotten to add the query string</li>
	<li>We could have a bad character in the query</li>
	<li>We could be missing the appropriate authentication tokens/headers</li>
</ul>
Fortunately, the Open Library API does not require any authentication tokens. It's... well... "open".

But if you did have a service that required a JWT token for example, or Basic Auth, it would be good to cover the scenario in which that is missing or improperly formatted.

Let's modify `fetchBooks` again:
```javascript
const fetchBooks = async (query) => {
  let response
  
  try {
    response = await request
      .get('http://openlibrary.org/search.json')
      .query({ q: query }) // query string
  } catch(e) {
    response = e.status
  }

  if (response.status === 404) return null
  if (response.status === 500) throw new Error('Open Library service down')
  if (response.status >= 400) throw new Error('Problem with request')
  else return response
}
```
Because there are lots of different HTTP response codes we could cover, and we could write lots of conditional checks to handle each one, here we just specify `if (response.status >= 400)` to catch all of the `Bad Request` 400-level codes.

And the tests:
```javascript
    it('should throw an error if there is a problem with the request (i.e. - 401 Unauthorized)', async () => {
      nock('http://openlibrary.org')
        .get('/search.json')
        .query(true)
        .reply(401)

      await expect(fetchBooks('lord of the rings')).to.be.rejected
    })

    it('should throw an error if there is a problem with the request (i.e. - 400 Bad Request)', async () => {
      nock('http://openlibrary.org')
        .get('/search.json')
        .query(true)
        .reply(400)

      await expect(fetchBooks('lord of the rings')).to.be.rejected
    })
```
<h3>Scenario 5: What if our application doesn't need the whole response?</h3>
What if our application doesn't need the whole response? What if we just need, say, the book titles?

We would need a filter/format function. In `book.service.js`, let's add a `getBookTitles` function and add it to the book service exports:
```javascript
const getBookTitles = (searchResults) => {
  return searchResults.map(({title_suggest}) => title_suggest)
}

module.exports = {
  fetchBooks,
  getBookTitles
}
```
And the test:
```javascript
  describe('getBookTitles', () => {
    it('should filter down response object to just book titles', () => {
      const titles = getBookTitles(booksFixture.docs)
      expect(titles).to.deep.equal([
        'The Lord of the Rings',
        'Lord of the Rings',
        'The Fellowship of the Ring'
      ])
    })
  })
```
<h3>Wrapping up</h3>
Here is the list of tests in their entirety:
```javascript
const chai = require('chai')
const chaiAsPromised = require('chai-as-promised')
const nock = require('nock')

const { booksFixture } = require('./books.fixture')

const { fetchBooks, getBookTitles } = require('../src/book.service')

const expect = chai.expect
chai.use(chaiAsPromised)

describe('Book Service', () => {
  describe('fetchBooks', () => {
    it('should return list of books based on search string', async () => {
      nock('http://openlibrary.org')
        .get('/search.json')
        .query(true)
        .reply(200, booksFixture)

      const {body} = await fetchBooks('lord of the rings')
      expect(body).to.deep.equal({
        docs: [
          {title_suggest: 'The Lord of the Rings', cover_edition_key: 'OL9701406M'},
          {title_suggest: 'Lord of the Rings', cover_edition_key: 'OL1532643M'},
          {title_suggest: 'The Fellowship of the Ring', cover_edition_key: 'OL18299598M'}
        ]
      })
    })

    it('should throw an error if the service is down', async () => {
      nock('http://openlibrary.org')
        .get('/search.json')
        .query(true)
        .reply(500)

      await expect(fetchBooks('lord of the rings')).to.be.rejectedWith('Open Library service down')
    })

    it('should return null if query returns a 404', async () => {
      nock('http://openlibrary.org')
        .get('/search.json')
        .query(true)
        .reply(404)

      const response = await fetchBooks('aksdfhkahsdfkhsadkfjhskadjhf')
      expect(response).to.be.null;
    })

    it('should throw an error if there is a problem with the request (i.e. - 401 Unauthorized)', async () => {
      nock('http://openlibrary.org')
        .get('/search.json')
        .query(true)
        .reply(401)

      expect(fetchBooks('lord of the rings')).to.be.rejectedWith('Problem with request')
    })

    it('should throw an error if there is a problem with the request (i.e. - 400 Bad Request)', async () => {
      nock('http://openlibrary.org')
        .get('/search.json')
        .query(true)
        .reply(400)

      await expect(fetchBooks('lord of the rings')).to.be.rejectedWith('Problem with request')
    })
  })

  describe('getBookTitles', () => {
    it('should filter down response object to just book titles', () => {
      const titles = getBookTitles(booksFixture.docs)
      expect(titles).to.deep.equal([
        'The Lord of the Rings',
        'Lord of the Rings',
        'The Fellowship of the Ring'
      ])
    })
  })
})
```

Next time you're writing a Node application that calls out to an external REST API, use this recipe as a starting point for writing your tests. It will make it much easier to know <strong>what</strong> to cover and help you figure out how to write your code.

<h3>One last thing!</h3>
Testing took me a while to figure out - there's not a lot of good tutorials out there explaining both the <strong>how</strong> and <strong>what</strong> of testing.

I'm trying to help make it easier. Because as soon as I figured out how to write just a few tests, it "snowballed" from there, and writing tests became much easier. And maybe... even... fun?

There are more testing posts on the way - if you want to learn not only <strong>how</strong> to test but <strong>what</strong> to test, <a href="https://www.coreycleary.me/about/">sign up to my newsletter</a> to be notified as soon as the next post is released!