Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to Stub a Chained Function #360

Closed
MedoHaleem opened this issue Apr 18, 2018 · 2 comments
Closed

How to Stub a Chained Function #360

MedoHaleem opened this issue Apr 18, 2018 · 2 comments

Comments

@MedoHaleem
Copy link

Description

googleMaps functions can support promise by adding .AsPromise() for example:

googleMapsClient.geocode({address: '1600 Amphitheatre Parkway, Mountain View, CA'})
  .asPromise()
  .then((response) => {
    console.log(response.json.results);
  })
  .catch((err) => {
    console.log(err);
  });

Issue

this how the function I want to stub look like

   let places = await googleMaps.placesNearby({
        location: data.coordinates,
        radius: data.radius,
        keyword: data.keyword
      }).asPromise()

And this is how I try to stub it

 beforeEach(() => {
      td.replace(googleMaps, 'placesNearby')
      td.when(googleMaps.placesNearby(td.matchers.anything())).thenReturn([[], ''])
      return PlaceService.searchForPlaces({'coordinates': [-7.7797637, 110.3888081], 'keyword': 'sushi', 'radius': 500}, fakeUser.id).then(r => result = r)
    })

I get the following Error

TypeError: googleMaps.placesNearby(...).asPromise is not a function
adding .asPromise() inside td.when, won't resolve the issue and give different error message


TypeError: Cannot read property 'asPromise' of undefined
    at Context.beforeEach (F:/Bibimapp/test/services/placeService.spec.js:192:15)

And I can't stub googleMaps.placesNearby because it is a function and not an object.

Environment

  • [ v9.5.0] node -v output:
  • [5.6.0 ] npm -v (or yarn --version) output:
  • [-- testdouble@3.7.0] npm ls testdouble

The issue can be recreated by installing GoogleMap package and try to stub any function as promise

@searls
Copy link
Member

searls commented Apr 28, 2018

So, first, to answer your question, this is how that can be achieved. I just tested this locally

function subject (googleMapsClient) {
  return googleMapsClient.placesNearby({
    location: 'some coords',
    radius: 42,
    keyword: 'tacos'
  }).asPromise()
}


const td = require("./lib");
const assert = require('assert')

async function test () {
  // Smell 1: dependency takes two steps for setup, good deps don't
  const googleMaps = require('@google/maps')
  const googleMapsClient = googleMaps.createClient()

  // Smell 2: mocking a third-party library. Any pain experienced is useless pain
  td.replace(googleMapsClient, 'placesNearby')

  // Smell 3: stubs-returning stubs. A good dep should do its job in one step
  td.when(googleMapsClient.placesNearby({
    location: 'some coords',
    radius: 42,
    keyword: 'tacos'
  })).thenReturn({
    asPromise: td.when(td.func('asPromise')()).thenResolve('some places')
  })

  const result = await subject(googleMapsClient)

  assert.equal(result, 'some places')
}

test()

However, I think this is a good test case for why you shouldn't mock third-party dependencies. I talk about this at greater length in my recent talk about mocking smells, too.

Instead, what I recommend is creating a wrapper of the third-party library's functionality you want and mocking that. So in this case that might look like:

// wrap/maps.js
const googleMaps = require('@google/maps')

module.exports = {
  placesNearby: function (options) {
    const googleMapsClient = googleMaps.createClient({key: process.env['GOOGLE_API_KEY']})
    return googleMapsClient.placesNearby(options).asPromise()
  }
}
// subject.js
const maps = require('./wrap/maps')

module.exports = function subject (options) {
  return maps.placesNearby(options)
}
//test.js
const td = require("./lib");
const assert = require('assert')

async function test () {
  const maps = td.replace('./wrap/maps')
  const subject = require('./subject')

  const options = {
    location: 'some coords',
    radius: 42,
    keyword: 'tacos'
  }
  td.when(maps.placesNearby(options)).thenResolve('some places')

  const result = await subject(options)

  assert.equal(result, 'some places')
}

test()

Now the contracts are clear and nothing was hard or awkward to set up. You'll also find yourself building an explicit contract in that wrap/maps.js adapter module of exactly what you depend on about Google Maps. So if you should ever choose to switch to MapBox or some GIS system, you will have a clear specification in hand.

@searls searls closed this as completed Apr 28, 2018
@carlbennettnz
Copy link
Contributor

Thanks for the full run down @searls. Completely agree with the wrapper concept.

That said, occasionally I do still find myself wanting to mock a chained API. For better or worse, I put together a little helper util, td-chain, to make that process a little more succinct.

const chain = require('td-chain')

const opts = {
  location: 'some coords',
  radius: 42,
  keyword: 'tacos'
}

td.when(
  chain(googlePlacesClient.placesNearby)(opts).asPromise()
  // for comparison, the real call: googlePlacesClient.placesNearby(opts).asPromise()
).thenResolve('some places')

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants