Hone logo
Hone
Problems

Mocking Subscription Services with Jest

When building applications that rely on external services, especially asynchronous ones like subscription management, it's crucial to test your code's interaction with these services without actually making real-world calls. This challenge focuses on creating a robust mock for a hypothetical subscription service using Jest. This will allow you to test features like user signup, plan upgrades, and cancellation logic in isolation.

Problem Description

You are tasked with creating a Jest mock for a SubscriptionService. This service is responsible for managing user subscriptions, including checking their status, creating new subscriptions, and processing cancellations. Your mock should accurately simulate the behavior of this service, allowing you to test components that depend on it.

Key Requirements:

  • checkSubscriptionStatus(userId: string): Promise<boolean>: This method should simulate checking if a user has an active subscription. It should return true if the user has an active subscription and false otherwise.
  • createSubscription(userId: string, plan: string): Promise<void>: This method should simulate creating a new subscription for a user with a specified plan. For the mock, simply resolving the promise is sufficient.
  • cancelSubscription(userId: string): Promise<void>: This method should simulate canceling a user's subscription. Again, for the mock, simply resolving the promise is sufficient.
  • State Management: The mock needs to maintain a simple internal state to track which users have active subscriptions and their associated plans.

Expected Behavior:

  • When checkSubscriptionStatus is called for a user who has had createSubscription called, it should return true.
  • When checkSubscriptionStatus is called for a user who has not had createSubscription called, or whose subscription has been cancelSubscriptioned, it should return false.
  • createSubscription should add the user to the mock's internal tracking of active subscriptions.
  • cancelSubscription should remove the user from the mock's internal tracking of active subscriptions.

Edge Cases to Consider:

  • Calling checkSubscriptionStatus for a user that has never existed in the mock's system.
  • Calling cancelSubscription for a user who does not have an active subscription.
  • Calling createSubscription for a user who already has an active subscription (the mock can choose to overwrite or ignore this, but a consistent behavior is expected).

Examples

Example 1: Basic Subscription Flow

// Assume SubscriptionService is defined elsewhere and we are mocking it.
// For this example, let's imagine we have a function that uses the service.

async function handleUserSignup(userId: string, subscriptionService: SubscriptionService) {
  const isActive = await subscriptionService.checkSubscriptionStatus(userId);
  if (!isActive) {
    await subscriptionService.createSubscription(userId, 'basic');
    return 'Subscription created successfully!';
  }
  return 'User already has an active subscription.';
}

// Mock setup (within a Jest test file)
const mockSubscriptionService = {
  subscriptions: new Map<string, string>(), // userId -> plan

  checkSubscriptionStatus: jest.fn((userId: string): Promise<boolean> => {
    return Promise.resolve(this.subscriptions.has(userId));
  }),

  createSubscription: jest.fn((userId: string, plan: string): Promise<void> => {
    this.subscriptions.set(userId, plan);
    return Promise.resolve();
  }),

  cancelSubscription: jest.fn((userId: string): Promise<void> => {
    this.subscriptions.delete(userId);
    return Promise.resolve();
  }),
};

// In a test:
// await handleUserSignup('user123', mockSubscriptionService as unknown as SubscriptionService);
// expect(mockSubscriptionService.createSubscription).toHaveBeenCalledWith('user123', 'basic');
// expect(await mockSubscriptionService.checkSubscriptionStatus('user123')).toBe(true);

Example 2: Cancelling a Subscription

// Assume a function that cancels a subscription.

async function cancelUserSubscription(userId: string, subscriptionService: SubscriptionService) {
  await subscriptionService.cancelSubscription(userId);
  return 'Subscription cancellation requested.';
}

// Mock setup for this scenario would be similar to Example 1, but we'd start with a user already subscribed.
// In a test:
// First, ensure the user is subscribed in the mock
// mockSubscriptionService.subscriptions.set('user456', 'premium');
//
// await cancelUserSubscription('user456', mockSubscriptionService as unknown as SubscriptionService);
// expect(mockSubscriptionService.cancelSubscription).toHaveBeenCalledWith('user456');
// expect(await mockSubscriptionService.checkSubscriptionStatus('user456')).toBe(false);

Example 3: Edge Case - Cancelling Non-existent Subscription

// In a test:
// For a user that has never subscribed
// expect(await mockSubscriptionService.checkSubscriptionStatus('nonexistentUser')).toBe(false);
//
// await cancelUserSubscription('nonexistentUser', mockSubscriptionService as unknown as SubscriptionService);
// // We expect cancelSubscription to be called, but it shouldn't throw an error.
// expect(mockSubscriptionService.cancelSubscription).toHaveBeenCalledWith('nonexistentUser');
// expect(await mockSubscriptionService.checkSubscriptionStatus('nonexistentUser')).toBe(false);

Constraints

  • The mock should be implemented using TypeScript.
  • Jest should be used for mocking capabilities (e.g., jest.fn()).
  • The mock should simulate asynchronous behavior using Promise.
  • The internal state of subscriptions should be managed within the mock object itself.

Notes

  • You'll need to define an interface for the SubscriptionService to properly type your mock.
  • Consider how you will reset the mock's internal state between different tests to ensure isolation. jest.clearAllMocks() and jest.resetAllMocks() can be useful here.
  • The this keyword within the mock functions might require careful handling depending on how you structure your mock object. Using arrow functions or binding this might be necessary.
  • Think about how you would inject this mock into the code that consumes it for testing purposes.
Loading editor...
typescript