Dependency injection ez-mode

When you're writing code that requires side-effecting dependencies (think libraries like axios), make your test-lyfe easier by adopting a lil' DI pattern, brought to you by vanilla JavaScript.

All that’s required is a closure. You’ll save yourself the grief of learning a mocking system like those that often come bundled with testing frameworks. While they can be useful, those mocking systems are often weirdly obtuse.

Say you have code like the following:

  import axios from 'axios'

  // Arbitrary example of something that looks real-world-ish
  export default function(baseURL) {
    const client = axios.create({
      baseURL,
      headers: { 'A-Header-That-I-Need': 'yep' }
    })

    return {
      get: endpoint => client.get(endpoint),
      post: (endpoint, payload) => client.post(endpoint, payload)
    }
  }

Looks pretty straight-forward, except that axios needs to be mocked during test time, as it produces side-effects like get and post. Let’s just circumnavigate the need for a mocking framework by using a closure for some dependency injection.

  import axios from 'axios'

  function _factory(theDependency) { // One extra closure
    return function(baseURL) {
      const client = theDependency.create({
        baseURL,
        headers: { 'A-Header-That-I-Need': 'yep' }
      })

      return {
        get: endpoint => client.get(endpoint),
        post: (endpoint, payload) => client.post(endpoint, payload)
      }
    }
  }

  export const Test = {
    _factory
  }

  // This right here. Just inject the dependency for the rest of the
  // application. The rest of your code won't tell the difference.
  export default _factory(axios)

From the perspective of the rest of our application code, nothing has changed, but unit testing just got easier. Unit tests can be written with the Test object instead of the default export, and we can more easily assert things about our code:

  import { Test } from 'out-http-client'

  // Some jest tests

  describe('http-client', () => {
    const get = jest.fn()
    const post = jest.fn()

    const fakeAxios = {
      create: () => ({
        get,
        post
      })
    }

    const client = Test._factory(fakeAxios)('https://an-base-url.biz')

    it('it calls `get`', () => {
      client.get(url)

      expect(get).toHaveBeenCalled()
    })

    // Et cetera
  })