tape-testing / tape

tap-producing test harness for node and browsers
MIT License
5.77k stars 307 forks source link

Sugggestions on how to mock a function in a Tape test. #548

Closed cagross closed 3 years ago

cagross commented 3 years ago

Hello.

I have a class method which has a secondary function defined inside of it (see code below). But the secondary function is designed to throw an exception when run in a Node environment. I'd like to run a Tape unit test to test this method. I don't need the secondary function at all in my test--it is irrelevant. But when I run my test (see test code below), as expected, an exception is thrown. With Tape, what are the suggested/typical ways to mock this secondary function, so I can still call this method in a unit test?

Is there a way to run my unit test and mock this secondary function (myFetchFunction) to a simple function that will not throw an error? In other words, during the unit test, set myFetchFunction() to some simple function, like console.log('Mocking function')

For example, I believe Jest has a way to do this, with their spyOn() method. I'm not complaining or demanding you have to be Jest :-) I'm just wondering how other Tape users have approached this.

Thanks in advance.

PS: I had a search through previous issues here, but nothing jumped out at me. Also, I didn't see anything in the README file about this. But after this exercise, if you want, I'd be happy to add a little blurb there, for future Tape users that might be wondering this.

edit: In hindsight, the function I want to mock in this case may be impossible to mock. Am I right there? If so, then I guess we can ignore the example I presented.


class.js

class myClass {
  myFunc() {
    let myVar
    myFetchFunc(var) {
      if (env === 'node') throw 'Cannot run in Node';
    }
    this.myVar = 555
    myFetchFunc(this.myVar)
  }
}
module.exports = { myClass }

test.js

const tape = require('tape')
const { myClass } = require('class')

tape('Tests description.', t => {
  const myObj= myClass()
  const actual = myObj.myVal
  const expected  = 555
  t.equal(actual, expected, 'Test description.')

  t.end()
})
ljharb commented 3 years ago

tape has no built-in mocking; you may want to use npmjs.com/sinon for this. You can also use npmjs.com/sinon-sandbox, as well as t.teardown(), to reset mocks after the test is done.

cagross commented 3 years ago

OK thanks very much for that. Do you think it would be helpful to add a blurb like this to the README? Not so much to inform people that Tape doesn't have built-in mocking--I think many will know/assume that. But moreso to point them in the right direction, to a solution that has worked well with Tape in the past (i.e. sinon). If so, let me know and I'd be happy to do that and submit a PR.

FYI I'm the same guy that submitted #541 and #542 :smiley:

ljharb commented 3 years ago

We could, but in the entire ecosystem everyone really only uses one of two options: sinon, or jest builtin mocking - so I’m not sure how much clarity we need to provide.

cagross commented 3 years ago

OK then, sounds good. At least now there's a GitHub issue in the Tape repo on the subject, for people that are clueless about mocking (like I was :smile:).

I'll close this :-)

Raynos commented 3 years ago

One approach you can take is to make non-trivial dependencies explicit. this makes the public API surface larger but makes writing tests simpler.

class myClass {
  constructor (options = {}) {
    this.myFetch = options.myFetch || (var) => {
      if (env === 'node') throw 'Cannot run in Node';
    }
  }

  myFunc() {
    let myVar
    this.myVar = 555
    this.myFetch(this.myVar)
  }
}
module.exports = { myClass }
tape('Tests description.', t => {
  const myObj= myClass({ myFetch: () => {} })
  const actual = myObj.myVal
  const expected  = 555
  t.equal(actual, expected, 'Test description.')

  t.end()
})

Another example of this pattern is using this.logger = options.logger vs a global logger. Using a global logger is super convenient and using options.logger is tedious but makes the code friendlier for tests.

ljharb commented 3 years ago

That’s indeed how you’d do it if you wanted dependency injection to be part of your api.

cagross commented 3 years ago

OK thanks for that dependency injection example. I knew dependency injection was an option, but hadn't yet taken the time to understand what it was exactly. Your example made it pretty clear to me, which I appreciate a lot :-)

That’s indeed how you’d do it if you wanted dependency injection to be part of your api.

OK noted. Before moving forward with this, I'd definitely chat with my manager about it.