Working on an ecommerce website or a marketplace app? Or maybe you need to test an Uber-like service or e-learning platform? These are different services, but they have one thing in common – you need to integrate a payment gateway like Stripe and, in most cases, test Stripe payments.
In this guide, I’m explaining how to test Stripe, what’s the right way to deal with its objects, and how to perform Stripe load and stability tests.
Let’s see how everything works.
How Does Stripe Work?
Stripe lets users pay for the content immediately: they don’t even need to register in your app or link their credit card with profiles. What they need to do is to enter cc credentials, and see what goes next:
- Credit card credentials go to Stripe
- Stripe tokenizes this info and returns the token to back-end
- Back-end then makes a charge
- All data is sent to Stripe, and Stripe shares this info with payment systems
- Payment system responds to Stripe (if everything’s alright) or reports issues (if not)
- Stripe then reports the state of the transaction
If the payment came through, users get access to the content. If it didn’t, they see an error message explaining what went wrong.
Now, let’s talk about Stripe features. I’m going to start with the most important ones – direct and destination charges.
Direct & Destination Charge
A direct charge means withdrawing money from the user’s account and sending them directly to your partner’s account.
Here’s how it works: imagine you’ve built an e-learning platform and hired educators to carry out classes. When a direct charge happens, students’ money goes directly to the teachers’ accounts. All the fees are also paid by the teacher, and your platform charges a fixed % from that sum.
In case of a destination charge, it’s your platform that pays Stripe fees, and net worth is kept on the platform’s account. Money is first transferred to your platform’s Stripe account then to teachers’ accounts.
Authorization and Capture
Stripe also supports two-step payments, which means you can authorize a charge fist and capture it later.
Uber is the best-known example, I guess. When you book a ride, you see a rough cost of the trip, which will stay frozen on your credit card until you finish the ride. As soon as you confirm you’ve completed the ride, Uber calculates the final cost of the trip and charges it from the card.
Transfers, Subscriptions, Refunds
Stripe also allows transfers from the platform account to suppliers. For instance, tutors need to link their Stripe account to their profiles to get paid.
Stripe supports subscriptions too, allowing you to set different intervals, trial periods, and so on to configure your subscription.
Finally, if the user wants to cancel their order or subscription, Stripe lets you refund the amount (fully or partially) directly to their credit cards.
How to Test Objects in Stripe
Now, I suggest taking a more detailed look at the Stripe objects.
Source object
The source object stores a payment method, the one Stripe will use to charge the customer.
Here’s the checklist for the source object:
Key | Value |
id | stripe_id of added card |
last4 | last 4 numbers of the card |
brand | credit card company (AE, Visa, MasterCard, etc.) |
exp_month, exp_year | card expiration date |
customer | their Stripe id |
When you’re testing mobile apps, make sure that a payment method corresponds with the returned value. You can do it by checking last4 and exp_month/year parameters.
If the source object is linked to a specific user and you want to make sure it’s the right user, check the customer’s Stripe id.
JSON of the source object:
{
"id": "card_1CboP4CLud4t5fBlZMiVrzBq",
"object": "card",
"address_city": null,
"address_country": null,
"address_line1": null,
"address_line1_check": null,
"address_line2": null,
"address_state": null,
"address_zip": null,
"address_zip_check": null,
"brand": "Visa",
"country": "US",
"customer": "cus_D1s9PQgvr6U46j",
"cvc_check": "pass",
"dynamic_last4": null,
"exp_month": 4,
"exp_year": 2024,
"fingerprint": "soMjdt25OvcMcObY",
"funding": "credit",
"last4": "4242",
"metadata": {},
"name": null,
"tokenization_method": null
}
Customer object
Key | Value |
id | customer stripe_id |
default_source | stripe_id of default card |
sources | list of Sources |
subscriptions | list of Subscriptions |
Customer object stores all the payment methods, including the default one, and keeps the information about users and their subscriptions.
Stripe also stores user’s cc and the payment method they set as default – and you can charge customers based on this info. Same with subscriptions: Stripe charges subscription fees automatically.
In Stripe, you can create webhooks for any event – that’s how you find out when the customer was charged or when an error showed up.
{
"id": "cus_D1s9PQgvr6U46j",
"object": "customer",
"account_balance": 0,
"created": 1528717303,
"currency": null,
"default_source": "card_1CboP4CLud4t5fBlZMiVrzBq",
"delinquent": false,
"description": null,
"discount": null,
"email": null,
"invoice_prefix": "4A178DE",
"livemode": false,
"metadata": {},
"shipping": null,
"sources": {
"object": "list",
"data": [
{
"id": "card_1CboP4CLud4t5fBlZMiVrzBq",
"object": "card",
"address_city": null,
"address_country": null,
"address_line1": null,
"address_line1_check": null,
"address_line2": null,
"address_state": null,
"address_zip": null,
"address_zip_check": null,
"brand": "Visa",
"country": "US",
"customer": "cus_D1s9PQgvr6U46j",
"cvc_check": "pass",
"dynamic_last4": null,
"exp_month": 4,
"exp_year": 2024,
"fingerprint": "soMjdt25OvcMcObY",
"funding": "credit",
"last4": "4242",
"metadata": {},
"name": null,
"tokenization_method": null
},
{
"id": "card_1CcC3uCLud4t5fBlW2UMknUW",
"object": "card",
"address_city": null,
"address_country": null,
"address_line1": null,
"address_line1_check": null,
"address_line2": null,
"address_state": null,
"address_zip": null,
"address_zip_check": null,
"brand": "Visa",
"country": "US",
"customer": "cus_D1s9PQgvr6U46j",
"cvc_check": "pass",
"dynamic_last4": null,
"exp_month": 4,
"exp_year": 2024,
"fingerprint": "soMjdt25OvcMcObY",
"funding": "credit",
"last4": "4242",
"metadata": {},
"name": null,
"tokenization_method": null
}
],
"has_more": false,
"total_count": 2,
"url": "/v1/customers/cus_D1s9PQgvr6U46j/sources"
},
"subscriptions": {
"object": "list",
"data": [],
"has_more": false,
"total_count": 0,
"url": "/v1/customers/cus_D1s9PQgvr6U46j/subscriptions"
}
}
Charge object
Key | Value |
id | charge stripe_id |
amount | amount paid, ¢ |
amount_refunded | amount refunded, ¢ |
customer | customer_id of the payer |
captured | true – payment processed, false – authorized |
destination | stripe account of payee |
Let’s take a more detailed look at these keys.
- amount – in cents, euro cents, and so on. Always check the amount customer was charged for.
- amount_refunded – you’ll see a value different from 0 only if the charge (or a part of it) was refunded to the user
- customer – customer’s id
- captured – shows transaction status: held on the user’s card or charged
- destination – stores the Stripe account of the user you’ve sent money to during a destination charge.
"fingerprint": "soMjdt25OvcMcObY",
"funding": "credit",
"last4": "4242",
"metadata": {},
"name": null,
"tokenization_method": null
},
"source_transfer": null,
"statement_descriptor": null,
"status": "succeeded",
"transfer_group": null
}
Refund object
Key | Value |
id | refund stripe_id |
amount | payment amount, ¢ |
status | success/pending/failed |
A refund object could be inbuilt in the charged object if the payment or a part of it was refunded to the user.
{
"id": "re_1CcY10CLud4t5fBlN23KtYq7",
"object": "refund",
"amount": 999,
"balance_transaction": "txn_1CcY10CLud4t5fBlhlmzzJuK",
"charge": "ch_1CcD7dCLud4t5fBlC1srZNIB",
"created": 1528892634,
"currency": "usd",
"metadata": {},
"reason": null,
"receipt_number": null,
"status": "succeeded"
}
Transfer object
Key | Value |
id | transfer_id |
amount | payout amount, ¢ |
destination | linked account (payee) |
reversed | false – transaction, true – funds reversal |
reversals | list of reverse transfer objects |
Keeps information about transfers from the platform balance to other accounts (to your teacher, if you have an e-learning platform).
All money transactions should be loginized in the database. This makes testing much easier: if QA engineers see the transfer id, they can go to Stripe and look through the parameters.
Let’s see how it works on examples. I’ll stick to the idea of the e-learning platform and teachers who are your payees.
- amount – sum paid to the teacher
- destination – Stripe account of the teacher
- reversed – shows a false value if the transaction came through and a true value if it’s reversed (when you need to cancel a transaction)
- reversals – keeps the list of objects if any part of your transfer was reversed
{
"id": "tr_1CcApyCLud4t5fBlZyx5mEPI",
"object": "transfer",
"amount": 250,
"amount_reversed": 0,
"balance_transaction": "txn_1CcApyCLud4t5fBlfA5cgXBz",
"created": 1528803538,
"currency": "usd",
"description": null,
"destination": "acct_18bAS3KcT341ksb9",
"destination_payment": "py_1CcApyKcT341ksb9VawxIJdS",
"livemode": false,
"metadata": {},
"reversals": {
"object": "list",
"data": [],
"has_more": false,
"total_count": 0,
"url": "/v1/transfers/tr_1CcApyCLud4t5fBlZyx5mEPI/reversals"
},
"reversed": false,
"source_transaction": null,
"source_type": "card",
"transfer_group": null
}
Balance Transaction object
Key | Value |
id | refund stripe_id |
amount | payment amount, ¢ (mind +/- signs) |
available_on | day when money will be available for a payee |
fee | Stripe fee |
fee_details | list of fee object |
net | net income/expenditure |
status | current status of the transaction |
type | transaction type |
Balance transaction stores details about any changes to your platform’s balance. You don’t actually need to test this object, but you should know where fees come from.
- amount – in cents; sometimes followed by + or – which shows the process of sending and receiving funds respectively
- available_on – shows when the money send to partners (teachers) will be available for them
- fee – amount of the Stripe fee
- fee_details – list of fee objects describing why this fee was charged
- net – net income
- status – status of operation success.
- type – stores a type of the object (charge, refund, or transfer).
Balance transaction (transfer):
{
"id": "txn_1CcApyCLud4t5fBlfA5cgXBz",
"object": "balance_transaction",
"amount": -250,
"available_on": 1528803538,
"created": 1528803538,
"currency": "usd",
"description": null,
"exchange_rate": null,
"fee": 0,
"fee_details": [],
"net": -250,
"source": "tr_1CcApyCLud4t5fBlZyx5mEPI",
"status": "available",
"type": "transfer"
}
Balance transaction (charge):
{
"id": "txn_1CbrRTCLud4t5fBlhRfMLdq1",
"object": "balance_transaction",
"amount": 10000,
"available_on": 1529280000,
"created": 1528728983,
"currency": "usd",
"description": "Charge user [email protected] for instructor [email protected] lesson id: 77",
"exchange_rate": null,
"fee": 320,
"fee_details": [
{
"amount": 320,
"application": null,
"currency": "usd",
"description": "Stripe processing fees",
"type": "stripe_fee"
}
],
"net": 9680,
"source": "ch_1CbrP3CLud4t5fBlztHMxVzv",
"status": "pending",
"type": "charge"
}
Subscription object
Key | Value |
id | subscription stripe_id |
application_fee_percent | % charged for subscription |
billing | automatic charge/sending invoice |
billing_cycle_anchor | time of the next subscription cycle |
current_period_start current_period_end | subscription timeframes |
plan | set of rules for subscription: amount, interval, trial days |
- application_fee_percent – % of the full amount the platform charges after users get a subscription. The rest of the money goes to the content owner.
- billing – shows how the billing process is organized: automatically or manually (via invoices)
- billing_cycle_anchor – contains the due date of the next payment (if the customer wants to keep their subscription)
- current_period_start & current_period_end – subscription validity dates
- plan – includes a set of rules like the amount users should pay for the subscription, its interval, number of trial days, and so on.
{
"id": "sub_D2JskPBqcW24hu",
"object": "subscription",
"application_fee_percent": null,
"billing": "charge_automatically",
"billing_cycle_anchor": 1528820423,
"cancel_at_period_end": false,
"canceled_at": null,
"created": 1528820423,
"current_period_end": 1531412423,
"current_period_start": 1528820423,
"customer": "cus_D2Jsi3JgT5zPh1",
"days_until_due": null,
"discount": null,
"ended_at": null,
"items": {
"object": "list",
"data": [
{
"id": "si_D2Js7N4mYxzAaY",
"object": "subscription_item",
"created": 1528820424,
"metadata": {
},
"plan": {
"id": "ivory-express-917",
"object": "plan",
"active": true,
"aggregate_usage": null,
"amount": 999,
"billing_scheme": "per_unit",
"created": 1528819224,
"currency": "usd",
"interval": "month",
"interval_count": 1,
"livemode": false,
"metadata": {
},
"name": "Ivory Express",
"nickname": null,
"product": "prod_D2JYysdjdQ2gwT",
"statement_descriptor": null,
"tiers": null,
"tiers_mode": null,
"transform_usage": null,
"trial_period_days": null,
"usage_type": "licensed"
},
"quantity": 1,
"subscription": "sub_D2JskPBqcW24hu"
}
],
"has_more": false,
"total_count": 1,
"url": "/v1/subscription_items?subscription=sub_D2JskPBqcW24hu"
},
"livemode": false,
"metadata": {
},
"plan": {
"id": "ivory-express-917",
"object": "plan",
"active": true,
"aggregate_usage": null,
"amount": 999,
"billing_scheme": "per_unit",
"created": 1528819224,
"currency": "usd",
"interval": "month",
"interval_count": 1,
"livemode": false,
"metadata": {
},
"name": "Ivory Express",
"nickname": null,
"product": "prod_D2JYysdjdQ2gwT",
"statement_descriptor": null,
"tiers": null,
"tiers_mode": null,
"transform_usage": null,
"trial_period_days": null,
"usage_type": "licensed"
},
"quantity": 1,
"start": 1528820423,
"status": "active",
"tax_percent": null,
"trial_end": null,
"trial_start": null
}
Load and Stability Testing
Finally, you may want to know how many transactions Stripe can process and if there’s any limit set. I recommend using GMeter to test these parameters.
Let’s see how you can test it:
- Create an event that launches the payment logic on the server-side.
- Check transaction logs in your database and its status using GMeter tools. Don’t forget that all transactions must be loginized in the database.
- Emulate these actions from n-users and specify the number of successful operations per specific period of time (like ten operations in one minute).
This way, you will see whether you need to optimize the payment logic, or it already works well.
That will be all! As you see, Stripe testing isn’t as hard as it may seem. You just need to make sure you understand how Stripe works and how to handle its objects the right way.