Stripe Client Library

In order to manage payments, we have been working on integrating the stripe go client library into our system. The library exposes CRUD operations on their resources; more or less a one to one mapping with their general api.

Peeking under the covers reveals that their library is little more than a wrapper for their HTTP api. The word little in the previous sentence is doing some heavy lifting; their library supports automatic retries, injected loggers, iterators, etc, but the point remains that the library is essentially an HTTP wrapper.

Backend Mocking

Given how HTTP reliant this library is, it follows that the recommended way of testing involves injecting a mock HTTP client backend into the parent client initialization or setting the backend on a global level.

From the project’s readme:

// Setup
stripe.Key = "sk_key"

stripe.SetBackend("api", backend) // optional, useful for mocking

or

// Setup
sc := &client.API{}
sc.Init("sk_key", nil) // the second parameter overrides the backends used if needed for mocking

Although stripe is well known for their robust documentation, they might have assumed that their users have a deep knowledge on HTTP clients, as there is very little written on how exactly to override the backend for testing. In fact, the only snippet I was able to find is this post on their issues page.

This makes a lot of sense if you want to test the full functionality of the library, specifically the automatic retries and the injected logging. Our team, however, used the library to do relatively simple things such as creating a charge or updating a customer. Unfortunately, the stripe library does not expose an interface for any of their resources.

Unit Testing Through Interfaces

Since the library is an abstraction for resource CRUD operations, and since we can see the method signatures for each of these operations, we can write our interface. By taking a look at how the client initializes each of these resources, we can create our own resource instead of relying on the client to create them, and inject them into structs expecting an interface. By doing so, we can inject a mocked resource into our abstraction and test it that way. Lets break that down

Creating our own interfaces

Since we know the resources and operations we are using ahead of time, we can define them through interfaces. The method signature can be borrowed from the stripe source code, since its exposed publically anyway. For example, if we know we will be creating and listing customers, we can define that operation as such

import (
  "github.com/stripe/stripe-go/v72"
  "github.com/stripe/stripe-go/v72/customer"
)

type customeriface interface {
  New(params *stripe.CustomerParams) (*stripe.Customer, error)
  List(params *stripe.CustomerListParams) *customer.Iter
}

Creating our own wrapper

Lets take another look at the source code, this time at the client code

a.Skus = &sku.Client{B: backends.API, Key: key}
a.Sources = &source.Client{B: backends.API, Key: key}
a.SourceTransactions = &sourcetransaction.Client{B: backends.API, Key: key}
a.Subscriptions = &sub.Client{B: backends.API, Key: key}

These individual resource clients are exposed through the global client. We can borrow this thinking when creating our “global client”, which in fact, is our own layer of abstraction in front of stripe. Since we defined behavior we need from stripe, we can define our abstraction layer as such, and use the new customer functionality we defined in our interface

import "github.com/stripe/stripe-go/v72"

type stripeAbstraction struct {
	cust        customeriface
}

func (s *stripeAbstraction) CreateCustomer(firstName, lastName, email string) (*stripe.Customer, error) {
	c, err := s.cust.New(&stripe.CustomerParams{
		Email: stripe.String(email),
		Name:  stripe.String(fmt.Sprintf("%s %s", firstName, lastName)),
	})
	if err != nil {
		return nil, err
	}
	return c, nil
}

We can then create an instance of stripeAbstraction by injecting the actual client

import "github.com/stripe/stripe-go/v72/customer"

sa := &stripeAbstraction{
  cust: &customer.Client{B: stripe.GetBackend("api"), Key: "my_stripe_secret_key"},
}

The actual customer client has the full gamut of CRUD operations, so it definitely fulfills the requirement of having the New and List operations.

Creating a mock resource

Creating the mock resource involves creating a struct that fits the definition of our interface. Mocking out the list functionality is little more interesting since it involves creating an iterator, but is still very much possible

import (
  "github.com/stripe/stripe-go/v72"
  "github.com/stripe/stripe-go/v72/customer"
)

type MockCust struct{}

func (mc *MockCust) New(custParams *stripe.CustomerParams) (*stripe.Customer, error) {
	if *custParams.Email == "badstripeemail@gmail.com" {
		return nil, fmt.Errorf("error creating stripe customer")
	}
	return &stripe.Customer{ID: "cust123"}, nil
}

func (mc *MockCust) List(params *stripe.CustomerListParams) *customer.Iter {
	iterator := stripe.GetIter(params, func(p *stripe.Params, b *form.Values) ([]interface{}, stripe.ListContainer, error) {
		list := &stripe.CustomerList{}

		switch *params.Email {
		case "no_customers@found.com":
			list.Data = []*stripe.Customer{}
		case "found@customers.com":
			list.Data = []*stripe.Customer{{Email: "found@customers.com"}}
		}

		ret := make([]interface{}, len(list.Data))
		for i, v := range list.Data {
			ret[i] = v
		}

		return ret, list, nil
	})
	return &customer.Iter{Iter: iterator}
}

Tying it all together

The only thing left to do to unit test stripeAbstraction would be to inject our mock customer resource. In our unit test file we do this as such

sa := &stripeAbstraction{cust: &MockCust{}}

So by defining our interfaces, we can pick and choose what stripe library functionalities we would like to use. This way of testing will not cover the intricacies of the library, but fitting with the go paradigm, it will make testing at least 80% of it very easy