How to Add Unit Tests to a Flask Stripe Payments Integration
Get the project source code below, and follow along with the lesson material.
Download Project Source CodeTo set up the project on your local machine, please follow the directions provided in the README.md
file. If you run into any issues with running the project source code, then feel free to reach out to the author in the course's Discord channel.
Testing
Because payments are a core part of our product, it's an important component to test. There is a small hitch though, a lot of our pages rely on making (authenticated) network requests to Stripe's API. Our test suite should be able to be run offline and not depend on an external API. To solve that, we can mock the API response. We saw a primitive example of doing that early on where we forced requests.get
to return a specific value.
A more robust way is to record what we expect network requests to return, and then use those in our test suite whenever a similar request is made. A library called "VCR.py" helps us do that, by allowing us to denote that certain tests should use "cassettes" of recorded network effects.
Add vcrpy
to your requirements.txt and run pip install -r requirements.txt
.
Our product details page now makes a request to Stripe's API. In order to capture what is being sent and received, we need to tell vcrpy
to record the network requests made during the execution of that test.
Since we've already defined our Stripe configuration variables in our terminal environment, when we run the tests, they will actually make network requests to Stripe. vcrpy
will observe these and write them into a folder.
Create a folder within yumroad/tests/
called cassettes
. This is where we will store recorded requests. We will also want vcrpy
to ignore sensitive authorization details so that we don't commit secrets into our codebase, so we can ask it to filter out authorization
headers.
In tests/test_product.py
, add a decorator to tell vcrpy
to monitor this test. The record_once
parameter will tell vcrpy
to let the network request go through and store the output if there isn't already a recorded file.
import vcr
...
@vcr.use_cassette('tests/cassettes/new_stripe_session.yaml',
filter_headers=['authorization'], record_mode='once')
def test_details_page(client, init_database):
book = create_book()
response = client.get(url_for('product.details', product_id=book.id))
assert response.status_code == 200
assert b'Yumroad' in response.data
assert b'Buy for $5.00' in response.data
assert book.name in str(response.data)
Then once you confirm that your environment variables are configured correctly, you can run your test suite with pytest --cov-report term-missing --cov=yumroad
. This run will create a file tests/cassettes/new_stripe_session.yaml
which will record details about the request and the response.
Take a look at the file to make sure it matches what you expect. An example cassette is provided with the source code included in the book.
Annotate the other tests in test_product.py
that make a network request.
@vcr.use_cassette('tests/cassettes/new_stripe_session.yaml',
filter_headers=['authorization'], record_mode='once')
def test_creation(client, init_database, authenticated_request):
response = client.post(url_for('product.create'),
data=dict(name='test product', description='is persisted', price=5),
follow_redirects=True)
assert response.status_code == 200
assert b'test product' in response.data
assert b'Buy for $5.00' in response.data
@vcr.use_cassette('tests/cassettes/new_stripe_session.yaml',
filter_headers=['authorization'], record_mode='once')
def test_post_checkout_success_page(client, init_database, user_with_product):
product = Product.query.first()
response = client.get(url_for('product.post_checkout', product_id=product.id,
status='success', session_id='test_1'), follow_redirects=True)
assert response.status_code == 200
assert b'You will receive an email shortly' in response.data
@vcr.use_cassette('tests/cassettes/new_stripe_session.yaml',
filter_headers=['authorization'], record_mode='once')
def test_post_checkout_fail_page(client, init_database, user_with_product):
product = Product.query.first()
response = client.get(url_for('product.post_checkout', product_id=product.id,
status='cancel', session_id='test_1'), follow_redirects=True)
assert response.status_code == 200
assert b'There was an error while attempting' in response.data
Now even if you go offline, these tests will still pass thanks to vcrpy
.
In order to test our webhooks, we have to be able to mock up signed webhook requests. To do that, we'll need to ensure that the TestConfig
in config.py
is using a specific key and write a few functions to mock what Stripe is doing their own end.
In config.py
:
class TestConfig(BaseConfig):
...
STRIPE_WEBHOOK_KEY = 'whsec_test_secret'
Create a test suite called test_checkout.py
in our tests folder.
We'll need a function to generate a signed header from arbitrary webhook data. This setup will help us get that.
import time
import json
from flask import url_for
import pytest
import stripe
import vcr
from yumroad.models import db, Product, Store, User
from yumroad.extensions import checkout
DUMMY_WEBHOOK_SECRET = 'whsec_test_secret'
def generate_header(payload, secret=DUMMY_WEBHOOK_SECRET, **kwargs):
timestamp = kwargs.get("timestamp", int(time.time()))
scheme = kwargs.get("scheme", stripe.WebhookSignature.EXPECTED_SCHEME)
signature = kwargs.get("signature", None)
if signature is None:
payload_to_sign = "%d.%s" % (timestamp, payload)
signature = stripe.WebhookSignature._compute_signature(
payload_to_sign, secret
)
header = "t=%d,%s=%s" % (timestamp, scheme, signature)
return {'Stripe-Signature': header}
def mock_webhook(event_name, data=None, webhook_secret=DUMMY_WEBHOOK_SECRET):
payload = {}
payload['type'] = event_name
payload['data'] = {}
payload['data']['object'] = data or {}
data = json.dumps(payload)
return data, generate_header(payload=data, secret=webhook_secret)
This lesson preview is part of the Fullstack Flask: Build a Complete SaaS App with Flask course and can be unlocked immediately with a single-time purchase. Already have access to this course? Log in here.
Get unlimited access to Fullstack Flask: Build a Complete SaaS App with Flask with a single-time purchase.

[00:00 - 01:41] Because payments are a core part of our product, it's an important piece to test. There is a hitch though. A lot of our pages rely on making real network requests to Stripe's API, and on top of that, there you need to be authenticated. Our test suite ideally should be able to run offline and not actually depend on whether or not an external API is up. So to do that, what we can do is mock the API response. Earlier in the course, we saw how we could do primitive mocking by just overriding what requests.get returns. However, this one is more complicated, and we're going to have to use the library to help us manage that. The library we're going to use is called vcrpy. And what it allows us to do is record cassettes of network requests. And instead of making real ones, we can just use the cassettes to mock the network requests. So get, we're going to get started by adding it to our requirements.txt from it's called vcrpy. And then in our terminal, we're going to go ahead and one, kit install dash r requirements dot txt. So the first thing we should do is just go ahead and run our existing test suite and see what happens. So I'm just going to run our existing test suite and see what fails. We're going to try and fix all the tests that failed before we try and record network requests or prevent duplicate network requests. Okay, so the user is going to test the failed. Let's go ahead and fix them one at a time, starting with test store . So here, the test home redirect failed. And the reason is because we had it go to a different page.
[01:42 - 02:08] So instead, I'm going to put this on test product, and I'm going to have it go to products dot index instead, because that's the change we made in the mid.py. Okay, so that solves that test. Now let's go back to test product and let's look at the test that failed. So specifically, the details page failed, and then test creation also failed. So it's looking for purchase.
[02:09 - 03:17] And let's look at what our template actually says here. It says buy for. So I think that's what the string we're going to have to look for. And then we're going to check the test edit submission page. And so let's see what test failed there. There was a ginger exception. One thing that's happening here is that when we create a sample book, it's probably expecting that we have a store as well. And when we create a sample book here, it doesn't mean we've created a store. So let's go and do that too. So we're going to say our store is equal to store, and its name is going to be store name. And that also means we're probably going to create a user as well. So let's go and do that too.
[03:18 - 06:08] All right, and then store here is. Okay, so this means all of our products will actually have a store now. Let's go and see what fails now. Okay, so I ran Py does again , only two things fail test detail page and test creation. And it's feeling specifically on the line where it says in the SERP shows up. The reason I might be doing that is because we're not setting a price on the product. So let's go back on the code and set a price. So here we're going to say price sense is equal to 5000. All right, let's go back to our test suite and run it now and see what happens. So our product is still failing. Let 's see what happens. Okay, so let's go back to our test suite. And then let's go back to our test suite. And then let's go back to our test suite. And then let's go back to our test area we're going to have to look at is on test product line 75. Here we're going to set test creation. And so here, if we don't set the price, we're actually not going to see the details of a product. So the details we're going to see is sold out. So let's go ahead and test that we see sold out on that page. All right, all of our tests pass now, which is great. There is an issue though, we're still making network requests every time we load the detail page. And to prove that we can add a print statement in our strike code and monitor what's going on. But instead of doing that, what we're going to do is we're going to rely on BCR to actually record the network request once when it happens, and then never have to do it again. So we have this cassettes folder. And that's where we're going to store these details. So when we test a detail page load, what we want to do is we want to test that there is a stripe session being created. And so since we know that's the network request that's going to be made, we're going to go test cassettes.
[06:09 - 08:05] And we're going to call this new stripe session dot yaml. Then we're going to want to not get the headers for authorization. Because that'll be secret. And we don't want to commit that to our repository. Then we also want to say that we want BCR dot py to record this once if it doesn't already exist. So if it's seeing a network request being made for the first time, you should log all of it into a cassette, but not do it again. Okay, then we're going to go ahead and also check some of the other pages where we see by as well. And I think it 's only on that page. So just leave it there is perfect for now. We're going to run pytest now and our test still passed. But if we go back in our code editor, we can see that stripe request was actually logged. So it's logging all of that detail. And it's coming back with here's the data that was returned. And now anytime we run our tests again, we will no longer have to issue a real request to stripe because the VCR knows what to look for, what kinds of requests to look for. Basically something that looks approximately like this , and is making a push request to check out slash sessions, it knows to respond with this instead. Okay, there's another few places where we can also test this. So we're going to want to test out some of the checkout success pages as well as the fail pages. And we're going to have to make sure that those actually return. And we're going to also improve the code coverage.
[08:06 - 08:50] So if we go back here and we run this with some code coverage, we're not going to get great results. So there's a bunch of places in our code that we're not at all testing. For example, we haven't tested any of the web hooks. We're missing some of the product code and some of the email code as well. So let's go in and write some more tests to solve that. In order to test the web hooks, we're going to create a new test file here called test checkout. And I'm going to import a bunch of things we're going to need. So we're going to reference the model and the checkout library quite a bit. Now next up is I'm going to set a test secret and a customer ID as well that we can use as constants.
[08:51 - 09:24] Now that I'm setting this as the webbook test secret, I should also tell our config to look at that as the test secret. So here, the default one is WHS test secret. And so we can make sure we're just using that one in our test. Great. Now we're going to have to generate stripe signature. So we're going to have to sign if we want to send data to our own webbook, we're going to have to sign it just like stripe signs it. So this function in the source code will help us do that. It'll essentially calculate a stripe signature.
[09:25 - 10:02] And then we can create a function called mock webbook that we can pass in data to, and then it will sign it accordingly. That means to test that we can actually parse a webhook. What we can do is create a mocked webhook and then call checkout dot parse web hook, which is that function that we have defined right here. So that tests whether our parsing is or correct. We can also test that'll complain when the webhook key is incorrect. Great.
[10:03 - 10:45] Now the next one is we also wrote this function in payments.py to actually get a customer. So I'm going to go back and test checkout and write a code. Write some code to get our test customer. Now for this dummy customer ID, I'm going to have to make that a real customer. Since we're issuing a real network request, I'm going to want to grab the ID of a real customer. So let's go into stripe and fetch one of our real customers . So this one should work. I'm just going to take the ID here and use that in our code.
[10:46 - 12:05] And we can check what the email we're looking for is. Okay. So now that we've set that to some test data, we can ensure that the get customer request here is going to match the data here. So let's go run the test at this point. All right. So this significantly improves our test coverage. And if we go back in our VS code editor, we can check out that a new cassette got created here and it's mocking the request to this customer endpoint. And it's returning the data that's stripe returned here. Okay. So now that we have a cassette for get customer, we can also build test out for other parts of our application . For example, we want to test that the event webhook is actually correct. And so this is more of a functional test. We're going to do a few things here. The first one is we're going to get a user with a product. And so this is a new fixture we're going to create. So it essentially does some of the things we did in test product where it creates a user, a store on a product.
[12:06 - 12:26] Let's go back in our fixtures and add that. So here's our fixture. It's going to create a user, a store and a product. Or also going to import product here. Great. Back in test checkout, we can now use that fixture. And everything else is already defined.
[12:27 - 13:14] So now this is going to test that when we hit the webhook with our dummy customer ID and client reference ID equal to the product ID, it's going to be able to parse that, return to 200 and send one email in the outbox. We can then go on to test that the email has a following subject and CC some other information as well. Okay. Okay. So I've also added this line that allows us to get the current webhook key and use that when we're signing the mock webhook. All right. Let's go into our terminal and run our tests and see what happens.
[13:15 - 14:12] So all of our tests are in. There's just a few lines of code we're missing. So I'm going to add a new one to test for the case when the product is not found. So here I can say instead of this, I'm going to do product plus two. And then when we make a response, I want this to assert that this is a 400 and we didn't send any emails. And I'm going to call this test not found product. So we're testing if this line is going to run. And to do that, we're also going to need abort. All right. Let's go and test if this works in our terminal. Okay. So that also passed. Let's see our new code coverage. All right. There's only seven lines of code and products.py that we haven't tested. Let's go check those out.
[14:13 - 14:55] So 59 through 66 here is basically this post checkout URL. So we can add two tests to make sure that the redirect happens as expected. So I've added two more tests here. What these do is they check the post checkout URL and follow redirects. And then they ensure that the page flashes the warning correctly and loads the product page. Now when we load the product page, we're going to create a new stripe session. So we're going to have to use this cassette as well. All right. Let's go and run these tests in our terminal and see if everything passes and if we're at 100% code coverage. Awesome. So there we are.
[14:56 - 15:00] We've implemented payments and also fully tested it.