14

So I am trying to mock all the stripe web hooks in the method so that I can write the Unit test for it. I am using the mock library for mocking the stripe methods. Here is the method I am trying to mock:

class AddCardView(APIView):
"""
* Add card for the customer
"""

permission_classes = (
    CustomerPermission,
)

def post(self, request, format=None):
    name = request.DATA.get('name', None)
    cvc = request.DATA.get('cvc', None)
    number = request.DATA.get('number', None)
    expiry = request.DATA.get('expiry', None)

    expiry_month, expiry_year = expiry.split("/")

    customer_obj = request.user.contact.business.customer

    customer = stripe.Customer.retrieve(customer_obj.stripe_id)

    try:
        card = customer.sources.create(
            source={
                "object": "card",
                "number": number,
                "exp_month": expiry_month,
                "exp_year": expiry_year,
                "cvc": cvc,
                "name": name
            }
        )
        # making it the default card
        customer.default_source = card.id
        customer.save()
    except CardError as ce:
        logger.error("Got CardError for customer_id={0}, CardError={1}".format(customer_obj.pk, ce.json_body))
        return Response({"success": False, "error": "Failed to add card"})
    else:
        customer_obj.card_last_4 = card.get('last4')
        customer_obj.card_kind = card.get('type', '')
        customer_obj.card_fingerprint = card.get('fingerprint')
        customer_obj.save()

    return Response({"success": True})

This is the method for unit testing:

@mock.patch('stripe.Customer.retrieve')
@mock.patch('stripe.Customer.create')
def test_add_card(self,create_mock,retrieve_mock):
    response = {
        'default_card': None,
        'cards': {
            "count": 0,
            "data": []
        }
    }

    # save_mock.return_value = response
    create_mock.return_value = response
    retrieve_mock.return_value = response

    self.api_client.client.login(username = self.username, password = self.password)
    res = self.api_client.post('/biz/api/auth/card/add')

    print res

Now stripe.Customer.retrieve is being mocked properly. But I am not able to mock customer.sources.create. I am really stuck on this.

Shubham
  • 3,071
  • 3
  • 29
  • 46

3 Answers3

17

This is the right way of doing it:


@mock.patch('stripe.Customer.retrieve')
def test_add_card_failure(self, retrieve_mock):
    data = {
        'name': "shubham",
        'cvc': 123,
        'number': "4242424242424242",
        'expiry': "12/23",
    }
    e = CardError("Card Error", "", "")
    retrieve_mock.return_value.sources.create.return_value = e

    self.api_client.client.login(username=self.username, password=self.password)

    res = self.api_client.post('/biz/api/auth/card/add', data=data)

    self.assertEqual(self.deserialize(res)['success'], False)
matusf
  • 469
  • 1
  • 8
  • 20
Shubham
  • 3,071
  • 3
  • 29
  • 46
5

Even though the given answer is correct, there is a way more comfortable solution using vcrpy. That is creating a cassette (record) once a given record does not exist yet. When it does, the mocking is done transparently and the record will be replayed. Beautiful.

Having a vanilla pyramid application, using py.test, my test now looks like this:

import vcr 
# here we have some FactoryBoy fixtures   
from tests.fixtures import PaymentServiceProviderFactory, SSOUserFactory

def test_post_transaction(sqla_session, test_app):
    # first we need a PSP and a User existent in the DB
    psp = PaymentServiceProviderFactory()  # type: PaymentServiceProvider
    user = SSOUserFactory()
    sqla_session.add(psp, user)
    sqla_session.flush()

    with vcr.use_cassette('tests/casettes/tests.checkout.services.transaction_test.test_post_transaction.yaml'):
        # with that PSP we create a new PSPTransaction ...
        res = test_app.post(url='/psps/%s/transaction' % psp.id,
                            params={
                                'token': '4711',
                                'amount': '12.44',
                                'currency': 'EUR',
                            })
        assert 201 == res.status_code
        assert 'id' in res.json_body
matusf
  • 469
  • 1
  • 8
  • 20
pansen
  • 341
  • 4
  • 5
  • I can only recommend people VCRpy. It is an super awesome library that helped us to reduce test time massively and most importantly it was very easy to integrate into our Django Tests – Amir Hadi Feb 08 '19 at 16:34
  • I've used VCR extensively with Ruby and didn't know there was a port in Python. Thank you, @pansen! – frankV Nov 02 '19 at 18:19
  • 1
    I can't imagine why anyone would recommend VCR for anything long term. It's the lazy way to do this, and you will have more problems maintaining/regenerating cassettes (no to mention storing all the extras in the responses in version control) than you will just doing it the right way and using something like httmock or equivalent to properly externalize dependencies. – JoeLinux Jan 26 '21 at 16:17
5

IMO, the following method is better than the rest of the answers

import unittest
import stripe
import json
from unittest.mock import patch
from stripe.http_client import RequestsClient # to mock the request session

stripe.api_key = "foo"

stripe.default_http_client = RequestsClient() # assigning the default HTTP client

null = None
false = False
true = True
charge_resp = {
    "id": "ch_1FgmT3DotIke6IEFVkwh2N6Y",
    "object": "charge",
    "amount": 1000,
    "amount_captured": 1000,
    "amount_refunded": 0,
    "billing_details": {
        "address": {
            "city": "Los Angeles",
            "country": "USA",
        },
        "email": null,
        "name": "Jerin",
        "phone": null
    },
    "captured": true,
}


def get_customer_city_from_charge(stripe_charge_id):
    # this is our function and we are writing unit-test for this function
    charge_response = stripe.Charge.retrieve("foo-bar")
    return charge_response.billing_details.address.city


class TestStringMethods(unittest.TestCase):

    @patch("stripe.default_http_client._session")
    def test_get_customer_city_from_charge(self, mock_session):
        mock_response = mock_session.request.return_value
        mock_response.content.decode.return_value = json.dumps(charge_resp)
        mock_response.status_code = 200

        city_name = get_customer_city_from_charge("some_id")
        self.assertEqual(city_name, "Los Angeles")


if __name__ == '__main__':
    unittest.main()

Advantages of this method

  1. You can generate the corresponding class objects (here, the charge_response variable is a type of Charge--(source code))
  2. You can use the dot (.) operator over the response (as we can do with real stripe SDK)
  3. dot operator support for deep attributes
JPG
  • 82,442
  • 19
  • 127
  • 206
  • 1
    This solution works for me, thanks!, now I am having throubles mocking error responses but its the best response. – Jovani Martinez Apr 09 '21 at 15:24
  • i am not sure how you would execute this for multiple stripe calls, given the mock seemingly is scoped to the single `stripe.Charge.retrieve("foo-bar")` call. – cevaris May 24 '23 at 03:17