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?
To adequately describe this solution, some background information is necessary.
The Overall Approach
Details
Mock the function in question
Interaction with the ChargeBee API is done through a simple:
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:Each thing in
resourcessupplies its own static functions that do everything needed.Customer, for example, has this:The
RequestWrapperobject contains therequestmethod that is doing the actual work. That method does something interesting:Basically,
q_1.defer()makes an object that contains aPromise, among other things. They usedeferred.promise.thento latch the code sent to the function to the overall API request, to be executed after thePromisebegins 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'screateDeferredfunction above isexported, so that can be leveraged to make something that closely follows ChargeBee's pattern.The overall mock is like so:
Important bits:
spyOnProperty, since the function is output as a property.getter.Util.createDeferredto 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.resolvedoesn't actually run until the overallPromiseis resolved later. Indeferred.resolve({list: []}), you are defining behavior which will happen when thePromiseis resolved later. Maybe obvious if you are intimately familiar with the order in whichresolves 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 ascallback; (c) In this specific example,callbackwill then runresolve(!res.list.find(payee => payee.customer.email === email)), producingresolve(false); (d)falseis the final result of the Promise chainCreate/import the tested function
I did this with a simple dynamic import after
spyOnPropertywas done. This ensures that the initialization codeconst chargebee = new ChargeBee();will use my alternate function provided inspyOnProperty. (Initialization code happens in./index: it is not shown.)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.