Pup

v2.x

TestingMocks

Mocks are an essential tool when writing tests. Mocks help us to "mock" code in our application so that when our tests run, we call a fake or "mocked" version of the code and not the real code. In Pup, mocks are defined for use in unit and integration tests while using Jest.

Mocking is important because typically, we want to only test our code, not the code our code depends on.

Consider this example: we need to test our function for subscribing a user to a plan via our payments provider, Stripe, works as expected. We should only have to test that our own code works (meaning, the code that calls Stripe's code for us). Conversely, we shouldn't be testing that Stripe's code works—that's their responsibility.

In the example on the right, we have a simple integration test written that expects the stripe.subscriptions.create() method to be called. By using a mock function, we can call our own function and when our test gets to that point in our code, it will substitute in the mocked version of stripe.subscriptions.create() instead of calling the actual method.

In our test, then, we can verify that our code did in fact call stripe.subscriptions.create() but we don't have to worry about actually calling it (leading to unwanted side effects).

/tests/__mocks__/stripe.js
const { subscription } = require('../fixtures/stripe/subscription');

module.exports = jest.fn(() => ({
  subscriptions: {
    create: jest.fn(() => ({ id: 'subscription123' })),
  },
}));
/api/Customers/actions/subscribe.js
/* eslint-disable consistent-return */

import Stripe from 'stripe';
import { Meteor } from 'meteor/meteor';
import Customers from '../Customers';

let action;

const stripe = Stripe(Meteor.settings.private.stripe);

const subscribeToPlan = (customer, plan) => {
  try {
    return stripe.customers.create({ customer, plan });
  } catch (exception) {
    throw new Error(`[subscribe.subscribeToPlan] ${exception.message}`);
  }
};

const getCustomer = (customerId) => {
  try {
    return Customers.findOne(customerId, { fields: { 'stripe.customerId': 1 } });
  } catch (exception) {
    throw new Error(`[subscribe.getCustomer] ${exception.message}`);
  }
};

const validateOptions = (options) => {
  try {
    if (!options) throw new Error('options object is required.');
    if (!options.customerId) throw new Error('options.customerId is required.');
    if (!options.plan) throw new Error('options.plan is required.');
  } catch (exception) {
    throw new Error(`[subscribe.validateOptions] ${exception.message}`);
  }
};

const subscribe = async (options) => {
  try {
    validateOptions(options);
    const customer = getCustomer(options.customerId);
    const subscription = await subscribeToPlan(customer.stripe.customerId, options.plan);
    action.resolve(subscription.id);
  } catch (exception) {
    throw new Error(`[subscribe] ${exception.message}`);
  }
};

export default (options) =>
  new Promise((resolve, reject) => {
    action = { resolve, reject };
    subscribe(options);
  });
/api/Customers/actions/subscribe.test.js
import stripe from 'stripe';
import subscribe from './subscribe';
import Customers from '../customers';

jest.mock('../customers');
Customers.findOne.mockImplementation(({ userId }) => ({ customerId: 'customer123', stripe: { customerId: 'stripeCustomerId123' } }));

describe('./subscribe.js', () => {
  beforeEach(() => {
    stripe.subscriptions.create.mockReset();
    stripe.subscriptions.create.mockImplementation(() => ({ id: 'subscription123' }));
  });

  test('creates subscription on stripe', async () => {
    await subscribe({ customerId: 'customer123', plan: 'mega-deluxe' });

    expect(stripe.subscriptions.create).toHaveBeenCalledTimes(1);
    expect(stripe.subscriptions.del).toHaveBeenCalledWith({ customer: 'stripeCustomerId123', plan: 'mega-deluxe' });
  });
});