Chargebee: how do I unit test chargebee-typescript using Jasmine?

203 Views Asked by At

I have a Firebase Cloud Function that checks whether an email exists in Chargebee. It works like this:

const cbCmd = chargeBee.customer.list({ email: { is: email }, include_deleted: false, limit: 1 });
const callbackResolver = new Promise<any>((resolve, reject) => {
  void cbCmd.request((err: any, res: WrappedListCustomerResp) => {
     if (err) {
       reject(err);
     }
     resolve(!res.list.find(payee => payee.customer.email === email));
     });
  });
return Promise.resolve(callbackResolver);

Basically, cbCmd contains a method called request which eventually runs the API request. request is sent a function that describes how I want to transform the data output by Chargebee. (Chargebee does not completely describe what they return in their documentation in their Typescript package. To describe the transformation competently, I researched the data types of what is returned and made my own interface.)

How do I unit test this using Jasmine?

1

There are 1 best solutions below

0
Joseph Zabinski On

To adequately describe this solution, some background information is necessary.

The Overall Approach

  1. Mock the function in question.
  2. Create/import the tested function.
  3. Test.

Details

Mock the function in question

Interaction with the ChargeBee API is done through a simple:

import {ChargeBee} from 'chargebee-typescript';
const chargebee = new ChargeBee();

All of the API methods are supplied this way. Under the hood, on the Javascript side, here is what happens for chargebee.customer, for example:

const resources = require("./resources");
class ChargeBee {
  get customer() {
        return resources.Customer;
    }
}

Each thing in resources supplies its own static functions that do everything needed. Customer, for example, has this:

class Customer extends model_1.Model {
    // OPERATIONS
    //-----------
    ...
    static list(params) {
        return new request_wrapper_1.RequestWrapper([params], {
            'methodName': 'list',
            'httpMethod': 'GET',
            'urlPrefix': '/customers',
            'urlSuffix': null,
            'hasIdInUrl': false,
            'isListReq': true,
        }, chargebee_1.ChargeBee._env);
    }
}

The RequestWrapper object contains the request method that is doing the actual work. That method does something interesting:

request(callBack = undefined, envOptions) {
  let deferred = util_1.Util.createDeferred(callBack);
  ...
  return deferred.promise;
}

static createDeferred(callback) {
        let deferred = q_1.defer();
        if (callback) {
            deferred.promise.then(function (res) {
                setTimeout(function () {
                    callback(null, res);
                }, 0);
            }, function (err) {
                setTimeout(function () {
                    callback(err, null);
                }, 0);
            });
        }
        return deferred;
    }

Basically, q_1.defer() makes an object that contains a Promise, among other things. They use deferred.promise.then to latch the code sent to the function to the overall API request, to be executed after the Promise begins to resolve.

To mock it, you need to override the prototype property of the getter in question. Return an alternate implementation of customer.list. Conveniently, ChargeBee's createDeferred function above is exported, so that can be leveraged to make something that closely follows ChargeBee's pattern.

The overall mock is like so:

spyOnProperty<any>(mockedChargebee.ChargeBee.prototype, 'customer', 'get').and.returnValue({
    list: () => ({
       request: (callBack?: any) => {
           const deferred = Util.createDeferred(callBack);
           deferred.resolve({ list: [] });
           return deferred.promise;
       }
    })
});

Important bits:

  • Although you are mocking a function, you still need spyOnProperty, since the function is output as a property.
  • You need to spy on the prototype so that later, your mocked prototype will be used during the object construction process later.
  • You need to specify that you are mocking a getter.
  • We are using Chargebee's Util.createDeferred to ensure that we are close to conforming to the same way that Chargebee does things. No guarantees here, but probably better to do it this way than rolling your own Promise details.
  • deferred.resolve doesn't actually run until the overall Promise is resolved later. In deferred.resolve({list: []}), you are defining behavior which will happen when the Promise is resolved later. Maybe obvious if you are intimately familiar with the order in which resolves are resolved; this was not obvious to me. The overall behavior here is: (a) Begin by sending {list: []} through the chain of Promises; (b) Send {list: []} as input to whatever is defined as callback; (c) In this specific example, callback will then run resolve(!res.list.find(payee => payee.customer.email === email)), producing resolve(false); (d) false is the final result of the Promise chain

Create/import the tested function

I did this with a simple dynamic import after spyOnProperty was done. This ensures that the initialization code const chargebee = new ChargeBee(); will use my alternate function provided in spyOnProperty. (Initialization code happens in ./index: it is not shown.)

const enableSignup = await import('./index').then(m => m.enableSignup);

Test

I happen to be working with Firebase, so I used a Firebase testing library to wrap the function in question. Then, test away using await.

const wrapped = firebaseTestFns.wrap(enableSignup);
const fnd = await wrapped('someemail');
expect(fnd).toBeTrue();