Giter Site home page Giter Site logo

testdouble.js's Introduction

testdouble.js (AKA td.js)

npmjs unpkg

Welcome! Are you writing JavaScript tests and in the market for a mocking library to fake out real things for you? testdouble.js is an opinionated, carefully-designed test double library maintained by, oddly enough, a software agency that's also named Test Double. (The term "test double" was coined by Gerard Meszaros in his book xUnit Test Patterns.)

If you practice test-driven development, testdouble.js was designed to promote terse, clear, and easy-to-understand tests. There's an awful lot to cover, so please take some time and enjoy our documentation, which is designed to show you how to make the most out of test doubles in your tests.

This library was designed to work for both Node.js and browser interpeters. It's also test-framework agnostic, so you can plop it into a codebase using Jasmine, Mocha, Tape, Jest, or our own teenytest.

Install

$ npm install -D testdouble

If you just want to fetch the browser distribution, you can also curl it from unpkg.

We recommend requiring the library in a test helper and setting it globally for convenience to the shorthand td:

// ES import syntax
import * as td from 'testdouble'

// CommonJS modules (e.g. Node.js)
globalThis.td = require('testdouble')

// Global set in our browser distribution
window.td

(You may need to configure your linter to ignore the td global. Instructions: eslint, standard.)

If you're using testdouble.js in conjunction with another test framework, you may also want to check out one of these extensions:

Getting started

Mocking libraries are more often abused than used effectively, so figuring out how to document a mocking library so as to only encourage healthy uses has proven to be a real challenge. Here are a few paths we've prepared for getting started with testdouble.js:

Of course, if you're unsure of how to approach writing an isolated test with testdouble.js, we welcome you to open an issue on GitHub to ask a question.

API

td.replace() and td.replaceEsm() for replacing dependencies

The first thing a test double library needs to do is give you a way to replace the production dependencies of your subject under test with fake ones controlled by your test.

We provide a top-level function called td.replace() that operates in two different modes: CommonJS module replacement and object-property replacement. Both modes will, by default, perform a deep clone of the real dependency which replaces all functions it encounters with fake test double functions which can, in turn, be configured by your test to either stub responses or assert invocations.

For ES modules, you should use td.replaceEsm(). More details here.

Module replacement with Node.js

td.replace('../path/to/module'[, customReplacement])

If you're using Node.js and don't mind using the CommonJS require() function in your tests (you can still use import/export in your production code, assuming you're compiling it down for consumption by your tests), testdouble.js uses a library we wrote called quibble to monkey-patch require() so that your subject will automatically receive your faked dependencies simply by requiring them. This approach may be familiar if you've used something like proxyquire, but our focus was to enable an even more minimal test setup.

Here's an example of using td.replace() in a Node.js test's setup:

let loadsPurchases, generatesInvoice, sendsInvoice, subject
module.exports = {
  beforeEach: () => {
    loadsPurchases = td.replace('../src/loads-purchases')
    generatesInvoice = td.replace('../src/generates-invoice')
    sendsInvoice = td.replace('../src/sends-invoice')
    subject = require('../src/index')
  }
  //…
  afterEach: function () { td.reset() }
}

In the above example, at the point when src/index is required, the module cache will be bypassed as index is loaded. If index goes on to subsequently require any of the td.replace()'d dependencies, it will receive a reference to the same fake dependencies that were returned to the test.

Because td.replace() first loads the actual file, it will do its best to return a fake that is shaped just like the real thing. That means that if loads-purchases exports a function, a test double function will be created and returned. If generates-invoice exports a constructor, a constructor test double will be returned, complete with test doubles for all of the original's static functions and instance methods. If sends-invoice exports a plain object of function properties, an object will be returned with test double functions in place of the originals' function properties. In every case, any non-function properties will be deep-cloned.

There are a few important things to keep in mind about replacing Node.js modules using td.replace():

  • The test must td.replace() and require() everything in a before-each hook, in order to bypass the Node.js module cache and to avoid pollution between tests
  • Any relative paths passed to td.replace() are relative from the test to the dependency. This runs counter to how some other tools do it, but we feel it makes more sense
  • The test suite (usually in a global after-each hook) must call td.reset() to ensure the real require() function and dependency modules are restored after each test case.
Default exports with ES modules

If your modules are written in the ES module syntax and they specify default exports (e.g. export default function loadsPurchases()), but are actually transpiled to CommonJS, just remember that you'll need to reference .default when translating to the CJS module format.

That means instead of this:

loadsPurchases = td.replace('../src/loads-purchases')

You probably want to assign the fake like this:

loadsPurchases = td.replace('../src/loads-purchases').default

Property replacement

td.replace(containingObject, nameOfPropertyToReplace[, customReplacement])

If you're running tests outside Node.js or otherwise injecting dependencies manually (or with a DI tool like dependable), then you may still use td.replace to automatically replace things if they're referenceable as properties on an object.

To illustrate, suppose our subject depends on app.signup below:

app.signup = {
  onSubmit: function () {},
  onCancel: function () {}
}

If our goal is to replace app.signup during a test of app.user.create(), our test setup might look like this:

let signup, subject
module.exports = {
  beforeEach: function () {
    signup = td.replace(app, 'signup')
    subject = app.user
  }
  // …
  afterEach: function () { td.reset() }
}

td.replace() will always return the newly-created fake imitation, even though in this case it's obviously still referenceable by the test and subject alike with app.signup. If we had wanted to only replace the onCancel function for whatever reason (though in this case, that would smell like a partial mock), we could have called td.replace(app.signup, 'onCancel'), instead.

Remember to call td.reset() in an after-each hook (preferably globally so one doesn't have to remember to do so in each and every test) so that testdouble.js can replace the original. This is crucial to avoiding hard-to-debug test pollution!

Specifying a custom replacement

The library's imitation feature is pretty sophisticated, but it's not perfect. It's also going to be pretty slow on large, complex objects. If you'd like to specify exactly what to replace a real dependency with, you can do so in either of the above modes by providing a final optional argument.

When replacing a Node.js module:

generatesInvoice = td.replace('../generates-invoice', {
  generate: td.func('a generate function'),
  name: 'fake invoices'
})

When replacing a property:

signup = td.replace(app, 'signup', {
  onSubmit: td.func('fake submit handler'),
  onCancel: function () { throw Error('do not call me') }
})

td.func(), td.object(), td.constructor(), td.instance() and td.imitate() to create test doubles

td.replace()'s imitation and injection convenience is great when your project's build configuration allows for it, but in many cases you'll want or need the control to create fake things directly. Each creation function can either imitate a real thing or be specified by passing a bit of configuration.

Each test double creation function is very flexible and can take a variety of inputs. What gets returned generally depends on the number and type of configuration parameters passed in, so we'll highlight each supported usage separately with an example invocation:

td.func()

The td.func() function (also available as td.function()) returns a test double function and can be called in three modes:

  • td.func(someRealFunction) - returns a test double function of the same name, including a deep imitation of all of its custom properties
  • td.func() - returns an anonymous test double function that can be used for stubbing and verifying any calls against it, but whose error messages and debugging output won't have a name to trace back to it
  • td.func('some name') - returns a test double function named 'some name', which will appear in any error messages as well as the debug info returned by passing the returned test double into td.explain()
  • td.func<Type>() - returns a test double function imitating the passed type. Examples and more details can be found in using with TypeScript

td.object()

The td.object() function returns an object containing test double functions, and supports three types of invocations:

  • td.object(realObject) - returns a deep imitation of the passed object, where each function is replaced with a test double function named for the property path (e.g. If realObject.invoices.send() was a function, the returned object would have property invoices.send set to a test double named '.invoices.send')
  • td.object(['add', 'subtract']) - returns a plain JavaScript object containing two properties add and subtract that are both assigned to test double functions named '.add' and '.subtract', respectively
  • td.object('a Person'[, {excludeMethods: ['then']}) - when passed with no args or with a string name as the first argument, returns an ES Proxy. The proxy will automatically intercept any call made to it and shunt in a test double that can be used for stubbing or verification. More details can be found in our full docs
  • td.object<Interface>() - returns an object with methods exposed as test doubles that are typed according to the passed interface. Examples and more details can be found in using with TypeScript

td.constructor()

If your code depends on ES classes or functions intended to be called with new, then the td.constructor() function can replace those dependencies as well.

  • td.constructor(RealConstructor) - returns a constructor whose calls can be verified and whose static and prototype functions have all been replaced with test double functions using the same imitation mechanism as td.func(realFunction) and td.object(realObject)
  • td.constructor(['select', 'save']) - returns a constructor with select and save properties on its prototype object set to test double functions named '#select' and '#save', respectively

When replacing a constructor, typically the test will configure stubbing & verification by directly addressing its prototype functions. To illustrate, that means in your test you might write:

const FakeConstructor = td.constructor(RealConstructor)
td.when(FakeConstructor.prototype.doStuff()).thenReturn('ok')

subject(FakeConstructor)

So that in your production code you can:

const subject = function (SomeConstructor) {
  const thing = new SomeConstructor()
  return thing.doStuff() // returns "ok"
}

td.instance()

As a shorthand convenience, td.instance() function will call td.constructor() and return a new instance of the fake constructor function it returns.

The following code snippets are functionally equivalent:

const fakeObject = td.instance(RealConstructor)
const FakeConstructor = td.constructor(RealConstructor)
const fakeObject = new FakeConstructor()

td.imitate()

td.imitate(realThing[, name])

If you know you want to imitate something, but don't know (or care) whether it's a function, object, or constructor, you can also just pass it to td.imitate() with an optional name parameter.

td.when() for stubbing responses

td.when(__rehearsal__[, options])

Once you have your subject's dependencies replaced with test double functions, you'll want to be able to stub return values (and other sorts of responses) when the subject invokes the test double in the way that the test expects.

To make stubbing configuration easy to read and grep, td.when()'s first argument isn't an argument at all, but rather a placeholder to demonstrate the way you're expecting the test double to be invoked by the subject, like so:

const increment = td.func()
td.when(increment(5)).thenReturn(6)

We would say that increment(5) is "rehearsing the invocation". Note that by default, a stubbing is only satisfied when the subject calls the test double exactly as it was rehearsed. This can be customized with argument matchers, which allow for rehearsals that do things like increment(td.matchers.isA(Number)) or save(td.matchers.contains({age: 21})).

Also note that, td.when() takes an optional configuration object as a second parameter, which enables advanced usage like ignoring extraneous arguments and limiting the number of times a stubbing can be satisfied.

Calling td.when() returns a number of functions that allow you to specify your desired outcome when the test double is invoked as demonstrated by your rehearsal. We'll begin with the most common of these: thenReturn.

td.when().thenReturn()

td.when(__rehearsal__[, options]).thenReturn('some value'[, more, values])

The simplest example is when you want to return a specific value in exchange for a known argument, like so:

const loadsPurchases = td.replace('../src/loads-purchases')
td.when(loadsPurchases(2018, 8)).thenReturn(['a purchase', 'another'])

Then, in the hands of your subject under test:

loadsPurchases(2018, 8) // returns `['a purchase', 'another']`
loadsPurchases(2018, 7) // returns undefined, since no stubbing was satisfied

If you're not used to stubbing, it may seem contrived to think a test will know exactly what argument to pass in and expect back from a dependency, but in an isolated unit test this is not only feasible but entirely normal and expected! Doing so helps the author ensure the test remains minimal and obvious to future readers.

Note as well that subsequent matching invocations can be stubbed by passing additional arguments to thenReturn(), like this:

const hitCounter = td.func()
td.when(hitCounter()).thenReturn(1, 2, 3, 4)

hitCounter() // 1
hitCounter() // 2
hitCounter() // 3
hitCounter() // 4
hitCounter() // 4

td.when().thenResolve() and td.when().thenReject()

td.when(__rehearsal__[, options]).thenResolve('some value'[, more, values])

td.when(__rehearsal__[, options]).thenReject('some value'[, more, values])

The thenResolve() and thenReject() stubbings will take whatever value is passed to them and wrap it in an immediately resolved or rejected promise, respectively. By default testdouble.js will use whatever Promise is globally defined, but you can specify your own like this:

td.config({promiseConstructor: require('bluebird')})`

Because the Promise spec indicates that all promises must tick the event loop, keep in mind that any stubbing configured with thenResolve or thenReject must be managed as an asynchronous test (consult your test framework's documentation if you're not sure).

td.when().thenCallback()

td.when(__rehearsal__[, options]).thenCallback('some value'[,other, args])

The thenCallback() stubbing will assume that the rehearsed invocation has an additional final argument that takes a callback function. When this stubbing is satisfied, testdouble.js will invoke that callback function and pass in whatever arguments were sent to thenCallback().

To illustrate, consider this stubbing:

const readFile = td.replace('../src/read-file')
td.when(readFile('my-secret-doc.txt')).thenCallback(null, 'secrets!')

Then, the subject might invoke readFile and pass an anonymous function:

readFile('my-secret-doc.txt', function (err, contents) {
  console.log(contents) // will print 'secrets!'
})

If the callback isn't in the final position, or if the test double also needs to return something, callbacks can be configured using the td.callback argument matcher.

On one hand, thenCallback() can be a great way to write fast and clear synchronous isolated unit tests of production code that's actually asynchronous. On the other hand, if it's necessary to verify the subject behaves correctly over multiple ticks of the event loop, you can control this with the defer and delay options.

td.when().thenThrow()

td.when(__rehearsal__[, options]).thenThrow(new Error('boom'))

The thenThrow() function does exactly what it says on the tin. Once this stubbing is configured, any matching invocations will throw the specified error.

Note that because rehearsal calls invoke the test double function, it's possible to configure a thenThrow stubbing and then accidentally trigger it when you attempt to configure subsequent stubbings or verifications. In these cases, you'll need to work around it by re-ordering your configurations or catch'ing the error.

td.when().thenDo()

td.when(__rehearsal__[, options]).thenDo(function (arg1, arg2) {})

For everything else, there is thenDo(). thenDo takes a function which will be invoked whenever satisfied with all the arguments and bound to the same this context that the test double function was actually invoked with. Whatever your thenDo function returns will be returned by the test double when the stubbing is satisfied. This configuration is useful for covering tricky cases not handled elsewhere, and may be a potential extension point for building on top of the library's stubbing capabilities.

td.verify() for verifying interactions

td.verify(__demonstration__[, options])

If you've learned how to stub responses with td.when() then you already know how to verify an invocation took place with td.verify()! We've gone out of our way to make the two as symmetrical as possible. You'll find that they have matching function signatures, support the same argument matchers, and take the same options.

The difference, then, is their purpose. While stubbings are meant to facilitate some behavior we want to exercise in our subject, verifications are meant to ensure a dependency was called in a particular expected way. Since td.verify() is an assertion step, it goes at the end of our test after we've invoked the subject under test.

A trivial example might be:

module.exports = function shouldSaveThings () {
  const save = td.replace('../src/save')
  const subject = require('../src/index')

  subject({name: 'dataz', data: '010101'})

  td.verify(save('dataz', '010101'))
}

The above will verify that save was called with the two specified arguments. If the verification fails (say it passed '010100' instead), testdouble.js will throw a nice long error message to explain how the test double function was actually called, hopefully helping you spot the error.

Just like with td.when(), more complex cases can be covered with argument matchers and configuration options.

A word of caution: td.verify() should be needed only sparingly. When you verify a function was called (as opposed to relying on what it returns) you're asserting that your subject has a side effect. Code with lots of side effects is bad, so mocking libraries are often abused to make side-effect heavy code easier to proliferate. In these cases, refactoring each dependency to return values instead is almost always the better design approach. A separate test smell with verifying calls is that sometimes—perhaps in the interest of maximal completeness—a test will verify an invocation that already satisfied a stubbing, but this is almost provably unnecessary.

td.listReplacedModules() for listing the modules that were replaced

td.listReplacedModules()

Use td.listReplacedModules() to list the modules that are replaced. This function will return an array of the modules that are currently being replaced via td.replace() or td.replaceEsm().

The list is in no particular order, and returns the full path to the module that was replaced. The path is returned as a file: URL as is customary in ESM (this is true even if the replaced module was CJS).

For example, if you do this:

td.replace('../src/save')

Then

td.listReplacedModules()

will return something like:

['file:///users/example/code/foo/src/save.js']

Other functions

For other top-level features in the testdouble.js API, consult the docs directory:

testdouble.js's People

Contributors

alexlafroscia avatar conradbeach avatar davemo avatar duluca avatar giltayar avatar hanneskaeufler avatar jakxz avatar jasonkarns avatar jasonmit avatar johnbotris avatar lgandecki avatar lisaah avatar lumaxis avatar marchaos avatar maxbeatty avatar miyu avatar mkusher avatar mohaalak avatar pke avatar primarilysnark avatar randycoulman avatar robwold avatar rosston avatar samjonester avatar schoonology avatar searls avatar turbo87 avatar webstech avatar woldiejdi avatar zzak avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

testdouble.js's Issues

Allow when & verify to disregard arity

#33 implements expected arg length matches actual invocation, which should have been implemented all along (oversight).

This means we should probably provide a way to stub or verify an interaction when we don't care about all the args to work around this. Whatever we do ought to be symmetrical between when & verify's API for ease of understanding.

Some ideas:

drive = td.create()

when(drive(), {ignoreAdditionalArgs: true}).thenReturn("💩")

drive() // "💩"
drive(1,3,4,5) // "💩"

So basically, the second argument to when could be an options object that's stored with the stubbing (or used by the verify) to modify how matching occurs. This gives us some breathing room for other features that would deviate from the default behavior, too.

One more example:

drive = td.create()

when(drive(60), {ignoreAdditionalArgs: true}).thenReturn("💩")

drive() // undefined
drive(60,1,3,4,5) // "💩"
drive(59,1,3,4,5) // undefined

Feature request: captors without verify

Forgive me if this isn't the appropriate place for feature request.

I've been writing a bunch of tests that involve the use of captors. I've found it's a little odd to use verify to capture the value of an anonymous function - especially when what you are verifying isn't really under test. For instance:

// verify callback is eventually called
var callback = td.function(), error = new Error(), data = {};
var captor = td.matchers.captor();

// calls mock.api with a argument and an anonymous function (that should invoke callback)
subject.delete(callback);

// capture the anonymous function (not a real verification), and call
td.verify(mock.api(td.matchers.anything(), captor.capture()));
captor.value(error, data);

// the REAL verification
td.verify(callback(error, data));

It struck me that this style might be a bit clearer:

// verify callback is eventually called
var callback = td.function(), args, anon, error = new Error(), data = {};

// calls mock.api with a argument and an anonymous function (that should invoke callback)
subject.delete(callback);

// capture the anonymous function and call
args = td.capture(mock.api);
anon = args.slice(-1);  // use the last argument
anon(error, data);

// verify
td.verify(callback(error, data));

To me this reads a bit more clearly, because I say what I'm doing - capturing the arguments that double was previously called with. It's entirely possible my assumptions are a bit off. I've only been using testdouble.js for 4 hours or so.

td.matchers.contains blows up on sparse matches

Just noticed this while writing docs:

matcher = td.matchers.contains({container: {size: 'S'}})

matcher.__matches({}) // blows up, can't check 'size' of undefined
matcher.__matches({container: {}}) // false does not blow up

We need to make sure that the contains matcher doing the deep object search fails fast when an intermediate property is absent

Stubbing callbacks

What if you could do this:

var fetch = td.function()

td.when(fetch('/some/path', td.callback(null, 'some data'))).thenReturn(undefined)

myFunction(fetch)

The td.callback API would just immediately invoke whatever function it captures at call-time with the provided arguments.

All so that you could synchronously specify this behavior in your source:

myFunction(fetch) {
  fetch('/some/path', function(er, data){
    console.log('my data is', data)
  }
}

Ensure last stubbing wins

Last-in-wins on stubs. For example:

td = require('testdouble');
testDouble = td.create();

td.when(testDouble(1)).thenReturn(5)
td.when(testDouble(1)).thenReturn(8)

testDouble(1) // returns 8

Add `thenDo`

Support invoking arbitrary functions when a stubbing is satisfied

when(testDouble(99)).thenDo(function(){ alert("SIDE EFFECTS!"); return "yay"; })

testDouble(99) // will return "yay" and also issue the window alert

give users a way to squelch individual warnings

Give users a local way to squelch warnings generated by the library. Ideally this should be local to either each rule or to each violation, to discourage users from globally disabling all warnings.

Hey @jasonkarns & @davemo any opinions on this?

One idea:

td.ignoreWarnings('optional excuse here', function(){
  td.when(somethingThatWouldTriggerAWarning()).thenReturn()
  //…
})

Warn when users verify an invocation they also stubbed

This is a super common error that we can detect and warn for. If you stub a particular invocation like fetch(42), then it means the subject needs that return value to do something else and the test's assertion should be about that something else. To also verify that fetch(42) was called ought to be redundant.

Is console.warn the best way to warn users in browsers & node? Should I throw an error but with a squelchWarnings config option?

Add `verify()`

Should be able to verify interactions on a test double, as a test spy.

testDouble =  require('testdouble').create();

verify(testDouble(5))  // throws error

One way to implement this might be to do the same trick we do for stubs (remember the last invocation on the double) and then exclude the most recent invocation when doing the verification check.

Re-use whatever arg-checker might be introduced by #1 so that argument matchers can be used in either stubbing or verifying

Add a `require('testdouble').explain` function

One thing that's always tough is debugging dynamic test doubles, in part b/c it can be problematic for them to implement a toString function of their own, and in part because any descriptors or names need to be provided to them by the caller (since they're not typically mirroring a known type). Additionally, we'll want to expose some metadata without adding methods or properties to the test double itself.

What if we had a top-level explain function that took a test double object and returned some nice descriptors:

td = require('testdouble')
myDouble = td.create()

td.explain(myDouble)

Might return:

{
  description: "This test double has 0 stubbings and has been called 0 times.",
  callCount: 0
  calls: []
}
td.when(myDouble(8,9,10).thenReturn(11)
td.explain(myDouble)

Might return:

{
  description: 
     "This test double has 1 stubbing and has been invoked 0 times. \n"+
     "\n"+
     "Stubbings: \n" +
     "  when called with (8,9,10) then return 11 \n" 
  callCount: 0
  calls: []
}

Changelog

Is there a changelog document somewhere that describes what happened between releases? if not it would be awesome if something like that could be created.

Implement argument matchers

This should work:

when(testDouble(any(Number)).thenReturn('yay');

testDouble(33) // 'yay'
testDouble("WOO") // undefined

The matcher API should be a super simple duck-typing thing so folks can define their own. See gimme as an example.

Is there a way to reset the state of a double?

I'm in need of something like:

td.clear(theDouble);

I'd love to just create a double every test, but I am using proxyquire to to inject "import" doubles which means I get one shot at creating a double option. Now the state of the double gets accumulated, which is not ideal.

Throws in node 0.10

In src/store/index.js you import events as EventEmitter. This change happened after node 0.10 and 0.12. As these versions are still LTS versions in mantaince mode, I need to continue to support testing them in my test suites.

Allow argument matchers to enhance failure messages

I was a little disappointed by this:

eatBiscuit(44)

td.verify(eatBiscuit(td.matchers.isA(Date)))

Error: Unsatisfied verification on test double.

  Wanted:
    - called with `({})`.

  But was actually called:
    - called with `(44)`.

It really seems like the isA matcher should have and use a hook to report something such that we'd see:

  Wanted:
    - called with `([something of type 'Date'])`.

Add verify times()

Once #4 is added, then consider adding:

verify(testDouble("lol"), times: 18)

To ensure this verification was satisfied 18 times.

Add a property-namespacing-like replace strategy for browsers.

Of course, td.replace() only works in Node.js, because quibble is coupled to Node's Module module. However, what if instead of throwing an error, the behavior was changed for browsers?

For instance:

var property = td.replace(some.object, 'property')

property // a test double created with the same rules as td.replace() for node

td.reset() // will restore all the replaced stuff

I like this a lot because it would increase the utility of testdouble.js on projects without marrying us to a module scheme. Conceivably, somebody could build a plugin using this as a primitive to add support for AMD or what-not.

Downsides to using the same td.replace name include possible confusion, however, since the behavior will be so different. I'm sure we could implement it without issue b/c browserify will load a different file, but I'm worried about the risk of confusion to users who might see a snippet or doc in one place which doesn't apply to the context they're using td.js in. JavaScript is hard.

What's the best way to use testdouble.js with tape?

I saw #33 but it's quite verbose to use testdouble with tape:

const test = require('tape');
const td = require('testdouble');

test('verify invoked', function(t) {
  t.plan(1)

  var stub = td.create()

  stub('any arguments', 0, 1, 2)

  t.doesNotThrow(function() {
    td.verify(stub0())
  }, 'called with more arguments when none are specified')
})

Anyway to not pass a callback to t.doesNotThrow?

Thanks!

Improve flow of getting started doc

Regarding this doc: https://github.com/testdouble/testdouble.js/blob/master/docs/3-getting-started.md#creating-a-test-double

We received these four chat messages as in-the-moment feedback:

this example is abit funky
…as the example, and MathProblem, doesn’t take constructor arg, but the test double example assumes it does
…ah i see, the later example implements the constructor
…kinda lost me until then

Look into these and see if we can pull forward what they need to make the doc read more naturally.

Reserved word

function is a reserved word. I think it would be prudent to deprecate td.function() in favor of some other method name, perhaps td.func().

The way the source is currently written, everything is actually valid. function is being used as an IdentifierName, which has no problem with reserved words:

module.exports =
  function: require('./function')

However if someone were to do something like this, they'd get a syntax error:

const td = require('test-double');
const { function } = td; // raises an exception

I feel like the above will become a fairly common use case, so by changing the method name to something that can be destructured into an Identifier without a reserved word clash would save some headaches:

const td = require('test-double');
const { func } = td; // this works

Of course, if someone wanted to destructure with the current name they could do it:

const { function: func } = td;

But I feel like that is forcing people into doing something for the sake of lexical compatibility instead of expressiveness, which is, I think, the use case for destructed re-assignment.

Anyways, this is just a suggestion. I understand it could introduce some backwards compatibility problems.

Getting testdouble.js to play nicely with Jasmine expectations?

I realize that this library is platform agnostic, but I wonder if anyone has any experience getting this library to play nicely with Jasmine's expectation engine? Sorry in advance if this is a bit off-topic. Im not sure where else to ask it.

If I have a simple spec which validates that something was called:

describe('The thing', () => {
  beforeEach(() => test.dependency = td.create(Dependency));
  beforeEach(() => test.subject = new DoesThing(test.dependency));

  it('fiddles the dependency', () => {
    test.subject.doThing('jake', 'stuff', 99);

    td.verify(test.dependency.fiddle({a: 'jake', b: 'stuff', c: 99}));
  });
});

Then Jasmine reports: "SPEC HAS NO EXPECTATIONS" and dumps an error to the console. It is very distracting.

I'd like to get Jasmine to see this verify call as an expectation.

Any thoughts on how to accomplish this?

Note when `ignoreExtraArgs` is used for a failed verification

Suppose you have this invocation and failed verification:

print('some', 'stuff', 'out', 'like', 8)

td.verify(print('some', 'stuff', 'NOPE'), {ignoreExtraArgs: true})

This will throw:

Error: Unsatisfied verification on test double.

  Wanted:
    - called with `("some", "stuff", "NOPE")`.

  But was actually called:
    - called with `("some", "stuff", "out", "like", 8)`.

It would be better if it printed a hint like:

  Wanted:
    - called with `("some", "stuff", "NOPE")`, ignoring any additional arguments.

td.replace() fails when dependency is cached

I have the following code that depends upon ./open-repository module:

git-commit.js

const root = require("app-root-path");

const openRepository = require(root + "/src/git/open-repository");

module.exports = function() {
  openRepository("./");
};

The following test check if the openRepository function is called with the "./" arguments:

git-commit.spec.js

const root = require("app-root-path");
const td = require("testdouble");

const gitCommit = require(root + "/src/routines/git-commit");

describe("git-commit", function() {
  "use strict";
  let openRepository;

  beforeEach(function() {
    openRepository = td.replace(root + "/src/git/open-repository");
  });

  afterEach(function(){
    td.reset();
  });

  it.only("should validate Github credentials", function() {
    gitCommit("Release version 0.0.1");
    td.verify(openRepository("./"));
  });

});

Unfortunately it fails with the following message:

1) git-commit should validate Github credentials:
     Unsatisfied verification on test double `/Users/fagnerbrack/Git/js-cookie-build/src/git/open-repository`.

  Wanted:
    - called with `("./")`.

  But there were no invocations of the test double.
  Error: Unsatisfied verification on test double `src/git/open-repository`.

    Wanted:
      - called with `("./")`.

    But there were no invocations of the test double.
      at Object.module.exports [as verify] (node_modules/testdouble/lib/verify.js:22:15)
      at Context.<anonymous> (test/unit/routines/git-commit.spec.js:22:8)

If I change git-commit.js like this, it works successfully:

const root = require("app-root-path");

module.exports = function() {
  const openRepository = require(root + "/src/git/open-repository");
  openRepository("./");
};

(Note that the require(root + "/src/git/open-repository"); is inside the module.exports declaration).

Is that the expected behavior? If so, can the td.replace() docs highlight why this doesn't work?

Building CoffeeScript at runtime is slow.

Its impact is roughly 200ms per runtime.

For a baseline, in a file td-repro.js I have

console.log('hi')

when ran, it gives the output

$ time node td-repro.js
hi

real    0m0.107s
user    0m0.083s
sys 0m0.016s

after an npm install testdouble, I replace the contents of td-repro.js with

var td = require('testdouble');
var myTestDouble = td.create();

myTestDouble('foo', 5)
td.verify(myTestDouble('foo', 5))
console.log('success')

and run again, with these results:

$ time node td-repro.js
success

real    0m0.369s
user    0m0.371s
sys 0m0.035s

But, if I

  • comment out the first line of index.js (rather node_modules/testdouble/index.js)
  • and execute coffee -c node_modules/testdouble/lib/*.coffee node_modules/testdouble/lib/store/*.coffee~

then the times are way down:

$ time node td-repro.js
success

real    0m0.190s
user    0m0.164s
sys 0m0.026s

From a baseline of ~110ms per run, a library that adds 70-80ms per run is incredible compared to one that adds 250ms.

Implement default matchers

A few matchers that should be supported out of the box after #1 and return them from require('testdouble').matchers

isA()

Should take a prototype and do whatever checking is necessary (beyond the buggy behavior of instanceof on primitive types) to ensure that something is-a.

when(testDouble(isA(Number), isA(String)).thenReturn("BOO")

testDouble(5, "hi") // "BOO"

argThat

when(testDouble(argThat(function(a) { return a.b === 5})).thenReturn(88)

testDouble({b: 5}) // 88
testDouble({b: "not five"}) // undefined

Other ideas

  • greaterThan
  • lessThan
  • startsWith
  • endsWith
  • contains (for array elements as well as subsets of objects)

Add `thenThrow`

Support throwing exceptions:

when(testDouble(7)).thenThrow(new Exception("WOOPS"))

testDouble(5) //undefined
testDouble(7) // throws "WOOPS"

Improve output for `times` verification failures

See this test:

      context '1 time, unsatisfied (with 2)', ->
        When -> @testDouble()
        And -> @testDouble()
        Then -> shouldThrow (=> td.verify(@testDouble(), times: 1)), """
          Unsatisfied verification on test double.

            Wanted:
              - called with `()` 1 time.

            But was actually called:
              - called with `()`.
              - called with `()`.
          """

I'd rather see it end with:

          """
            But was actually called:
              - called with `()` 2 times.
          """

This would require some care to make sure we account for matchers of different types of calls when we compare and print the actuals (since if the matchers satisfy they'll 'look' the same to the lib. We'd need a way to disable matchers for this purpose)

Add sequential stubbing

Often you want to stub different values for the first, second, third, etc., invocation of a test double. Support this with variable arguments on thenReturn

when(testDouble(8)).thenReturn(1,2,3)

testDouble(8) //1
testDouble() // undefined
testDouble(8) //2
testDouble(8) //3
testDouble(8) //3

Subsequent invocations beyond those specified will continue to return the last value.

When stringifying args, use a testdouble's name if it has one

If I want to verify that a testdouble object is called with an argument of another testdouble, it might be nice to show the name of the argument testdouble in the error message. An example:

Given:

var foo = td.create('named_foo');
var bar = td.create('named_bar');
td.verify(bar(foo));

the error message will be:

Error: Unsatisfied verification on test double `named_bar`.

  Wanted:
    - called with `(undefined)`.

  But there were no invocations of the test double.

It could be useful if the error message instead had the line:

    - called with `(named_foo)`.

This is related to a twitter conversation where toString() is also mentioned; I'm not 100% sure how that plays into it but that will probably become clear when I check out the source a bit more.

should anything() matcher swallow subsequent arity?

Try this:

drive = td.create()
td.when(drive(5, anything())).thenReturn('A')

drive()  //undefined, expected
drive(5)  //'A'
drive(5, 'hi') //'A'
drive(5, 'hi', 'bye')  //'A'
drive(6) // undefined, expected
drive(6, null) //undefined, expected

I'm torn on what drive(5) and drive(5, 'hi', 'bye') should do in this case. Right now anything() will slurp up all remaining args to just match any number, which isn't as strict as I'd personally like but would make it a bridge less far from sinon/jasmine's default-to-unconditional-stubbing.

Thoughts? @jasonkarns

td.replace() property replacement warning & failure states

Inspired by #86, just thinking out loud:

Property replacement should fail if:

  • The property does not exist and no manual replacement is passed
  • The property exists, no manual replacement is passed, and imitate doesn't know how to handle its type (currently imitate will return a td.function even if the property is something strange like a String, Number, null, etc.

Property replacement should warn if:

  • The property exists, manual replacement is used, and the type of the replacement doesn't match the type of replacement being passed

Is the added failure state a "breaking" change worthy of a major bump?

Implement Prototype for create()

When I call create and pass a prototypal function, I should expect to be returned an object with doubles for each of its prototype's functions.

For instance if I have:

Foo = function(){};
Foo.prototype.bar = function(){};
Foo.prototype.baz = function(){};

Then I should be able to:

testDouble = require('testdouble').create(Foo);
when(testDouble.bar(3)).thenReturn("LOL")
testDouble.bar(3) // "LOL" 

td.matchers.contains does not work on sparse object trees

I expected this to work:

var brew = td.function()

td.when(brew({container: {size: 'S'}})).thenReturn('small coffee')

brew({ingredient: 'beans', container: { type: 'cup', size: 'S'}}) // 'small coffee'

But instead, it didn't satisfy the stubbing. Looks like the contains matcher for plain objects isn't as relaxed as I thought. Gonna call this a bughancement

Using td.replace() with external modules

Based on the node example I've tried to use td.replace() with a external module. The module I'm testing does something like this:

import elasticsearch from 'elasticsearch'
var client = new elasticsearch.Client({ ... })
client.search({ ... });

My test, currently using sinon, does something like this:

import sinon from 'sinon'
import elasticsearch from 'elasticsearch'
searchSpy = {
  search: sinon.stub().returns(Promise.resolve({
    hits: {
      hits: [ ... ]
    }
  }))
}
sinon.stub(elasticsearch, 'Client').returns(searchSpy)

Stubbing only elasticsearch.Client is something I want to get rid of (to solve an unrelated problem). td.replace() seems like the right tool for that. So I tried something like this:

import elasticsearch from 'elasticsearch'
import td from 'testdouble'
searchSpy = {
  search: sinon.stub().returns(Promise.resolve({
    hits: {
      hits: [ ... ]
    }
  }))
}
var es = td.replace('elasticsearch')
td.when(es.Client()).return(searchSpy)

That fails with TypeError: es.Client is not a function. td.when(es.Client) doesn't work either.

Since there is no documentation for td.replace() and the example only imports internal module (with relative paths), I can't tell if I'm doing it wrong (likely) or if this is just not supported (yet, also likely).

If the snippets above aren't enough, I can also put together a sample that actually works.

Quibble in the browser

I'm trying to set up my project to run the tests in the browser for debugging purposes, using webpack-dev-server. When I do so, I run into issues with quibble. I note from f694c82 that there's been some work done to guard against requireing quibble in the browser, but there are two more unguarded references to it. One is in reset.coffee, and the other is in replace/module.coffee. I believe the former reference is causing my issue.

I'm seeing the following error message:

ERROR in ./~/quibble/lib/quibble.js
Module not found: Error: Cannot resolve module 'module' in /Users/randy/src/scratch-it/skraphaug/node_modules/quibble/lib
 @ ./~/quibble/lib/quibble.js 9:11-28

But looking at the stack trace in my browser, it looks like the require of quibble in reset.coffee is the root problem.

I'll be happy to provide any additional information or settings you need.

support promises resolve/reject

I got an email asking for promise support. I didn't think hard about it because I don't have an immediate need.

What if we had this API?

td.config(promiseProvider: someFuncThatCreatesPromises)

td.when(...).thenResolve(someValue)
td.when(...).thenReject(someOtherValue)

And then maybe on the verification side?

td.verify.resolved(value)
td.verify.rejected(someOtherValue)

I don't use promises much and I generally don't TDD with them in mind (since I try to keep async stuff to the periphery of my app where I'm integrating with stuff). Any thoughts on how to go about this or what an example test would look like?

Paging @jasonkarns as he's my promise whisperer

name a test double with `create("name")`

It will be helpful for some folks to differentiate two doubles in all messages if they have a name. In particular when #2 is ready, the name can be easily constructed as "#{type}##{functionName}" for nicer messages.

verify does not throw exception when too many arguments are passed

Verify doesn't throw exceptions when too many arguments are passed. Right now its only on too few arguments or collisions.

I expected td.verify(double('any', 'parameters')) to default to checking arity, but currently only fails on too few arguments. Here's an example reproduction:

In a file td-repro.js

var td = require('testdouble')
  , test = require('tape')

test('verify tests arity matches', function(t) {
  t.plan(4)

  var stub0 = td.create()
  var stub1 = td.create()
  var stub2 = td.create()
  var stub3 = td.create()

  t.doesNotThrow(function() {
    stub0('any arguments', 0, 1, 2)
    td.verify(stub0())
  }, 'called with more arguments when none are specified')

  t.throws(function() {
    stub1(0)
    td.verify(stub2(0,0))
  }, 'called with fewer arguments fails')

  t.doesNotThrow(function() {
    stub2(0,0)
    td.verify(stub2(0,0))
  }, 'called with the same number succeeds')

  t.throws(function() {
    stub3(0,0,1)
    td.verify(stub3(0,0))
  }, 'called with more arguments fails') //   <-- does not fail
})

The command node td-repro.js gives this output:

TAP version 13
# verify tests arity matches
ok 1 called with more arguments when none are specified
ok 2 called with fewer arguments fails
ok 3 called with the same number succeeds
not ok 4 called with more arguments fails
  ---
    operator: throws
    expected: undefined
    actual:   undefined
    at: Test.<anonymous> (/Users/matthew/Projects/scrap/td-bug.js:27:5)
  ...

1..4
# tests 4
# pass  3
# fail  1

Warn when arity of stubbing or verification doesn't match function.length

If I do a test double func:

var dog = {
  bite: function (thing) {}
}
var woof = td.replace(dog, 'bite')

Then setting up a stubbing with a different arity should warn:

td.when(dog.woof('mailman', 'spritely')).thenReturn("UH OH") // arity mismatch warning 

This warning should be able to be muted on a case-by-case basis since function .length is unreliable.

td.when(dog.woof('mailman', 'spritely'), {ignoreArity: true}).thenReturn("UH OH") // no warning

td.constructor() feature for creating fake constructors

This is already basically implemented to support td.replace() (source) of constructors, but it's not exposed in an explicit way.

Given a constructor:

function Dog(){}
Dog.prototype.bark = function(){}
Dog.prototype.woof = function(){}

One could call (in environments supporting destructured assignment):

var [FakeDog, dog] = td.constructor(Dog)

Where FakeDog is a constructor that can be passed to the subject or otherwise replaced and dog is a plain JavaScript object of the same set of test doubles one would get if they instantiated new FakeDog().

One point of potential confusion for users is that instantiating FakeDog multiple times will yield the same set of fake functions, so the total number of stubbings & invocations would accumulate over all FakeDog instances, which would run counter to our expectations of statefulness with instantiated objects, but would be necessary for the test to have sane access to the test double functions. Feedback on this point would be welcome.

td.replace() fails for method on prototype

var td = require('testdouble');

var Foo = function() {
  console.log('foo');
};

Foo.prototype.bar = function() {
  console.log('bar');
};

var foo = new Foo();

var bar = td.replace(foo, 'bar');

bar();

results in

Error: td.replace error: No "bar" property was found.

which is most likely related to the use of the hasOwnProperty() function to check if the property exists.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.