Working with the Stripe API is unlike most anything else in the world of software development. Stripe’s documentation is outstanding. Their API is intuitive and the language specific clients are easy to use. Each Stripe account comes with a test environment that makes development and integration testing effortless. The stripe
command line tool can be used for simple API queries, setting up authentication and testing of web hooks. On top of all that, there is stripe-mock
, a local service with a near- complete implementation of the API that can be used for testing and development without requiring a network connection to the Stripe Test environment.
The issue with stripe-mock–and with testing against (near) “real” versions of any API–is that it adds additional overhead to each test because you’ll likely have to use the API to define or tweak test data before it can be used. Once stripe-mock gets statefulness, that overhead will grow.
This is fine for end-to-end testing where having tests walk a pre-designed path of API interactions that follow real-world usage is a good thing to do, but for unit testing, this amount of overhead leads to heavy tests that are hard to read and difficult to write. Life teaches us that if something is hard, we’re less likely to do lots of it. That is bad mojo for good test coverage.
In past projects, I have seen several ways of dealing with this problem, from a test-library of pre-defined hardcoded fixture data, to full-on crazy Java-style builder factory monstrosities with class names that put the average German word length to shame.
I started looking at the internals of the Stripe python API client and found something very interesting. The JSON structure returned by the API is parsed into a Python dict
and then passed to a utility method that inspects the contents and builds the appropriate Stripe object hierarchy.
This sparked an idea. If I could define a Python dict
with the right elements and invoke that Stripe utility method, I’d be able to define and build Stripe objects on the fly. Sprinkle in a little bit of builder-pattern and I’d have a quick, easy and readable way to build all varieties of Stripe objects needed to test my code. That in combination with Mockito (a great mocking framework for Python) would make for a very functional and pleasant way of unit testing a Stripe integration built with Python. And that’s exactly what I did.
Let’s look at a quick example of this through an imaginary Stripe integration function that retrieves the amount of an upcoming invoice for a customer defined by an email address.
We’ll have to look up the customer in Stripe using the email address (bad practice, I know, but bear with me here) and then invoke the API endpoint that returns the upcoming invoice and grab its amount_due
field. Note that Customer.list
returns a collection of Customer objects, even if there’s only one element.
import stripe
def upcoming_amount_due(customer_email):
customers = stripe.Customer.list(email=customer_email)
customer = customers.data[0]
upcoming_invoice = stripe.Invoice.upcoming(customer=customer.id)
return upcoming_invoice.amount_due
Writing a test for this example would require setting up a fair amount of fixture data as well as several API calls to get the expected state, regardless of whether we would use Stripe’s test environment or stripe-mock
.
You’d have to create a Customer, ensure a Product structure is set up, then create a Subscription with one Subscription Item to your product and depending on the type of Subscription, you may even have to report some usage in order to trigger an upcoming invoice amount. I won’t bore you with a code example of all that, but I hope you agree that that would be a hassle.
Here’s how that same unit test would be written using the Stripe object builders and Mockito:
import stripe
from mockito import when
from unittest import TestCase
from my_stripe_implementation import upcoming_amount_due
from my_stripe_builders import *
class StripeUnitTest(TestCase):
def test_upcoming_amount_due(self):
test_email = "test@example.com"
expected_amount = 5000
customer_builder = StripeCustomerBuilder()
customer = customer_builder.build()
customer_list = StripeListBuilder().add(
customer_builder).build()
when(stripe.Customer).list(
email=test_email).thenReturn(customer_list)
when(stripe.Invoice).upcoming(
customer=customer.id).thenReturn(
StripeInvoiceBuilder()
.amount_due(expected_amount)
.build())
self.assertEqual(expected_amount, upcoming_amount_due(test_email))
Using a combination of pre-defined fixture data and overrides where needed, the object builders allow for quickly setting up test data. The code under test invokes the Stripe API client, but through the use of Mockito, it will receive the test objects.
Yes, for this test, we could have entirely mocked out the objects with Mockito, but when the code under tests uses more of the fields, or when it loops through list of API results, it quickly becomes easier to just build the “real thing”.
Now that you have an idea of how these builders are used, let’s take a look under the covers. At the heart of it all lies the base class, StripeObjectBuilder
.
import random
import string
from time import time
import stripe
class StripeObjectBuilder:
def _build_data(self):
return self._data
def __init__(self):
self._data = None
self._id_str = ''.join(random.choice(
string.ascii_letters + string.digits) for _ in range(16))
self._now_epoch_secs = int(time())
def _id(self, prefix: str):
return "{}_{}".format(prefix.replace("_", ""), self._id_str)
def build(self):
return stripe.util.convert_to_stripe_object(self._build_data())
def deleted(self):
self._data = dict(
id=self._data["id"],
object=self._data["object"],
deleted=True
)
return self
def __getattr__(self, field_name):
def set_data_field(value):
self._data[field_name] = value
return self
return set_data_field
The object structure will be held in the _data
field. _build_data()
finalizes building of the object structure (reason for this will become apparent when we look at the list object below) which is then passed to stripe.util.convert_to_stripe_object()
by build()
.
Builder classes for specific Stripe objects will extend StripeObjectBuilder
and define the object structure by setting the _data
field in their constructor.
__getattr__
allows overwriting any field in the _data
dict and because self
is returned, these methods can be chained for concise object construction syntax, as illustrated with .amount_due(expected_amount)
in the example above.
Many Stripe objects like Customer, Subscription, etcetera have a metadata map for arbitrary key/value storage. We can build convenient support for that with the following extension:
class StripeObjectWithMetadataBuilder(StripeObjectBuilder):
def __init__(self):
self._data = None
super().__init__()
def set_metadata(self, key: str, value):
if "metadata" not in self._data:
self._data["metadata"] = dict()
self._data["metadata"][key] = value
return self
Before we get to the Customer builder, we need to get one more bit of infrastructure out of the way; Lists. Enter the StripeListBuilder
:
class StripeListBuilder(StripeObjectBuilder):
def _build_data(self):
self._data["data"] = [item._build_data() for item in self._list_items]
return self._data
def __init__(self):
super().__init__()
self._list_items = []
self._data = dict(
object="list",
data=None,
has_more=False,
total_count=0,
url="",)
def add(self, builder: StripeObjectBuilder):
self._list_items.append(builder)
self._data["total_count"] += 1
return self
Items can be added to the list builder by passing their builder to the add()
method. Note again that self is returned for method chaining. _build_data()
loops through the _list_items
and builds the _data
structure.
Using this, we can now look at the StripeCustomerBuilder
:
class StripeCustomerBuilder(StripeObjectWithMetadataBuilder):
def _build_data(self):
self._data["sources"] = self._sources._build_data()
self._data["subscriptions"] = self._subscriptions._build_data()
self._data["tax_ids"] = self._tax_ids._build_data()
return self._data
def __init__(self):
super().__init__()
self._sources = StripeListBuilder()
self._subscriptions = StripeListBuilder()
self._tax_ids = StripeListBuilder()
self._data = dict(
id=self._id("cus"),
object="customer",
address=dict(
line1="123 main st",
line2="apt 4",
city="San Francisco",
country="US",
postal_code="94102",
state="CA"
),
currency="usd",
created=self._now_epoch_secs,
default_source=None,
description="unit test customer 123 ",
delinquent=False,
email="test_customer@company.com",
invoice_prefix="MYINVPREFIX",
name="Test Customer",
phone="41512367890",
preferred_locales=["en"],
sources=dict(),
subscriptions=dict(),
tax_exempt="exempt",
tax_ids=dict(),
)
def add_subscription_builder(
self, subscription_builder: StripeSubscriptionBuilder):
subscription_builder.customer(self._data["id"])
self._subscriptions.add(subscription_builder)
return self
The constructor defines a basic set of fixture data. Embedded objects, like subscriptions in the subscriptions list can be added using add_… methods (for brevity, those for sources
and tax_ids
are omitted).
The fixture _data
structure is based on the JSON hierarchy in the Customer API documentation. Any extra fields can be added as needed, depending on your implementation.
Using the Customer example above, it’s easy to write builders for other Stripe objects. I built them as my integration required new objects and found myself re-using them over and over again.
To round things out, here is another example of a complicated set of connected Stripe objects, defined in a few lines using Stripe Object Builders:
customer = (
StripeCustomerBuilder()
.name("John Doe")
.delinquent(True)
.email("jdoe@company.com")
.add_subscription_builder(
StripeSubscriptionBuilder()
.add_item_builder(StripeItemBuilder(
StripePlanBuilder()
.nickname("3dollarMontly")
.amount(300)))
.add_item_builder(StripeItemBuilder(
StripePlanBuilder()
.nickname("6dollarMonthly")
.amount(600)))
)
).build()
In 17 lines of code, we build a Customer called John Doe, who can be emailed at jdoe@company.com about his lack of payment for a Subscription to the 3 and 6 dollars a month plan.
Using object builders like these, I’ve written lots of self-contained, readable unit tests for my Stripe integration code. I’m not sure if the world needs it, but I have been toying with the idea of creating an open source library that takes the ideas outlined above, combined with Stripe’s OpenAPI fixture data, to provide an auto-generated full featured set of object builders for use in any Stripe integration.
As always, I hope this was useful to you. If it is, or if you have suggestions, be sure to drop me a line.