Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Stubbing stripe with sinon - using stub.yields

I'm trying to stub the nodejs stripe api with sinon, to test the creation of a customer using a test that looks like:

var sinon = require('sinon');
var stripe = require('stripe');
var controller = require('../my-controller');

var stub = sinon.stub(stripe.customers, 'create');
stub.create.yields([null, {id: 'xyz789'}]);
//stub.create.yields(null, {id: 'xyz789'}); //same result with or without array 

controller.post(req, {}, done);

My understanding is that stub.create.yields should call the first callback, and pass it (in this case) null, followed by an object with id of xyz789 This may be where I am mistaken

Inside my 'controller' I have the following:

exports.post = function(req, res, next) {

    stripe.customers.create({
        card: req.body.stripeToken,
        plan: 'standard1month',
        email: req.body.email
    }, function(err, customer) {

        console.log('ERR = ', err)
        console.log('CUSTOMER = ', customer)

err, and customer are both undefined.

Have I done something wrong?

EDIT

I think the issue could be around here: (stripe docs)

var stripe = require('stripe')(' your stripe API key ');

So, stripe constructor takes an api key

In my test, I'm not supplying one: var stripe = require('stripe');

But in my controller, I have:

var stripe = require('stripe')('my-key-from-config');

So, as per your example, I have:

test.js:

var controller = require('./controller');
var sinon = require('sinon');
var stripe = require('stripe')('test');

var stub = sinon.stub(stripe.customers, 'create');
stub.yields(null, {id: 'xyz789'});
//stub.create.yields(null, {id: 'xyz789'}); //same result with or without array 

controller.post({}, {}, function(){});

controller.js

var stripe = require('stripe')('my-key-from-config');

var controller = {
    post: function (req, res, done) {
        stripe.customers.create({
            card: req.body,
            plan: 'standard1month',
        }, function(err, customer) {
            console.log('ERR = ', err);
            console.log('CUSTOMER = ', customer);
        });
    }
}

module.exports = controller;
like image 549
Alex Avatar asked Mar 25 '14 20:03

Alex


People also ask

What does Sinon stub return?

The sinon. stub() substitutes the real function and returns a stub object that you can configure using methods like callsFake() . Stubs also have a callCount property that tells you how many times the stub was called.

What is callsFake in Sinon stub?

In Sinon, a fake is a Function that records arguments, return value, the value of this and exception thrown (if any) for all of its calls. A fake is immutable: once created, the behavior will not change. Unlike sinon. spy and sinon. stub methods, the sinon.

What is stubbing in JavaScript?

What are Stubs? A test stub is a function or object that replaces the actual behavior of a module with a fixed response. The stub can only return the fixed response it was programmed to return.


4 Answers

It looks like you're trying to isolate your #post() function from #stripe.customers.create(), in the Stripe API. @lambinator is correct in pointing out that the customers object is created, dynamically, when you call

require('stripe')('my-key-from-config')

and

require('stripe')('test')

so your stub, in the test, does not apply to #stripe.customers.create(), in the controller.

You could inject the test instance of stripe into the controller, as @lambinator suggests. Injection is pretty much the best thing ever. However, if you're writing a rubber-meets-the-road type component (like a proxy), injection is inappropriate. Instead, you can use the second export provided in the Stripe module:

Stripe.js:

...

// expose constructor as a named property to enable mocking with Sinon.JS
module.exports.Stripe = Stripe;

Test:

var sinon = require('sinon');
var stripe = require('stripe')('test');
var StripeObjectStub = sinon.stub(Stripe, 'Stripe', function(){
  return stripe;
});
//NOTE! This is required AFTER we've stubbed the constructor.
var controller = require('./controller');

var stub = sinon.stub(stripe.customers, 'create');
stub.create.yields([null, {id: 'xyz789'}]);
//stub.create.yields(null, {id: 'xyz789'}); //same result with or without array 

controller.post({}, {}, function(){});

Controller:

require('stripe').Stripe('my-key-from-config');

var controller = {
post: function (req, res, done) {
    stripe.customers.create({
        card: req.body,
        plan: 'standard1month',
    }, function(err, customer) {
        console.log('ERR = ', err);
        console.log('CUSTOMER = ', customer);
    });
}

Then #stripe.customers.create(), in your controller, will invoke your test stub.

like image 83
Angrysheep Avatar answered Oct 23 '22 01:10

Angrysheep


When you do this:

var stripe = require('stripe')('my-key-from-config');

The stripe library creates the customer and other objects dynamically. So, when you stub it out in one file:

test.js

var stripe = require('stripe')('test');
var stub = sinon.stub(stripe.customers, 'create');

And your controller creates another stripe instance to use in another file:

controller.js

var stripe = require('stripe')('my-key-from-config');
var controller = { ... }

The stubbed version from test has no impact on the controller's version.

SO....

You either need to inject the test instance of stripe into your controller, or use a library like nock to mock things at the http level, like so:

  nock('https://api.stripe.com:443')
    .post('/v1/customers', "email=user1%40example.com&card=tok_5I6lor706YkUbj")
    .reply 200, 
      object: 'customer'  
      id: 'cus_somestripeid'
like image 22
lambinator Avatar answered Oct 23 '22 02:10

lambinator


Based on @Angrysheep's answer which uses the exported Stripe constructor (the best method IMHO), here's a working code as of the time of writing this:

Controller

//I'm using dotenv to get the secret key
var stripe = require('stripe').Stripe(process.env.STRIPE_SECRET_KEY);

Test

//First, create a stripe object
var StripeLib = require("stripe");
var stripe = StripeLib.Stripe('test');

//Then, stub the stripe library to always return the same object when calling the constructor
const StripeStub = sinon.stub(StripeLib, 'Stripe').returns(stripe);

// import controller here. The stripe object created there will be the one create above
var controller = require('./controller');

 describe('a test', () => {
     let createCustomerStub;
     const stripeCustomerId = 'a1b2c3';

     before(() => {
        //Now, when we stub the object created here, we also stub the object used in the controller
        createCustomerStub = sinon.stub(stripe.customers, 'create').returns({id: stripeCustomerId});
    });

    after(() => {   
        createCustomerStub.restore();
    });
});

EDIT: based on a comment below, this approach might no longer work.

A reasonable approach to walk around this entire issue is to use Stripe as an injected dependency, i.e. to pass the Stripe object to the relevant object's constructor. This will allow injecting a stub as part of the testing suite.

like image 4
etov Avatar answered Oct 23 '22 01:10

etov


It is likely something that is not part of what you have described here.

yields is an alias for callsArg, but attempts to call the first argument that is a function and provides it the arguments using Function.prototype.apply - this means @psquared is correct in saying it does not need to be an array.

However, this isn't your problem. Attempting to recreate the given code in JSFiddle, we can see that it successfully calls back the argument.

var stripe = {
    customers: {
        create: function () {}
    }
};
var controller = {
    post: function (req, res, done) {
        stripe.customers.create({
            card: req.body,
            plan: 'standard1month',
        }, function(err, customer) {
            console.log('ERR = ', err);
            console.log('CUSTOMER = ', customer);
        });
    }
}

var stub = sinon.stub(stripe.customers, 'create');
stub.yields(null, {id: 'xyz789'});
//stub.create.yields(null, {id: 'xyz789'}); //same result with or without array 

controller.post({}, {}, function(){});

This tells me you need to show more of your code, or try writing a reduced test case to attempt to find where the problem lies.

like image 2
Keithamus Avatar answered Oct 23 '22 01:10

Keithamus