3

I'm trying to mock an instance method of an object contained in another object.

I got two objects

class CreditCard(object):
    def charge(self, amount):
        if amount < 0:
            raise Exception('Invalid amount')
        print "charging %d" % amount

class Transaction(object):
    def __init__(self, credit_card, amount):
        self.amount = amount
        self.credit_card = credit_card
        self.status = 'PRISTINE'

    def pay(self):
        self.credit_card.charge(self.amount)
        self.status = 'COMPLETE'

This is a simplified example of what i'm trying to acheive

class TestTransaction(TestCase):

    def setUp(self):
        cc = CreditCard()
        t = Transaction(cc, 10)

    def test_should_not_mark_transaction_as_complete_if_charge_failed(self):
        with mock.patch('t.credit_card.charge') as mock_charge:
            mock_charge.side_effect = Exception
            with self.assertRaises(Exception):
                t.pay()
            self.assertEquaL(t.status, 'PRISTINE')

There is a lot of logic inside charge so I'm trying to isolate the transaction test by mocking the charge.

Thanks, python 2.7

2 Answers 2

2

Since you are mocking method of a class, you can patch it directly:

import mock
import unittest

from your_code import CreditCard, Transaction

class TestTransaction(unittest.TestCase):
    def setUp(self):
        self.cc = CreditCard()
        self.t = Transaction(self.cc, 10)

    @mock.patch('your_code.CreditCard.charge', side_effect=Exception)
    def test_should_not_mark_transaction_as_complete_if_charge_failed(self, mock_charge):
        with self.assertRaises(Exception):
            self.t.pay()
        self.assertEqual(self.t.status, 'PRISTINE')

Be sure to udpate the module path in the patch, where are defined your classes.

Also, I corrected your initial example, due to several errors (use of global variables instead of instance attributes in the test case).

Sign up to request clarification or add additional context in comments.

Comments

0

Since you are passing a CreditCard instance into Transaction, just create a Mock object and pass that instead.

class TestTransaction(TestCase):

    def setUp(self):
         self.fake_cc = Mock()
         # set up any side effects, return values etc. of the fake cc
         # in your tests as needed...
         self.t = Transaction(self.fake_cc, 10)

    def test_should_not_mark_transaction_as_complete_if_charge_failed(self):
        self.fake_cc.side_effect = Exception
        with self.assertRaises(Exception):
            self.t.pay()

5 Comments

Thanks, i've tried that. I'm getting a ValueErrror if i dont pass CreditCard as spec, e.g Mock(spec=CreditCard), but even like that i'm getting a AttributeError: Mock object has no attribute '_state'. CreditCard is a related sjango model of transaction. I wonder if it's even possible.
Well, it should be possible. If you are seeing AttributeErrors, make sure that the mock mimics the real CreditCard instance well. There might also be other things that the pay() method depends on, try mocking these as well (what you want in general is to isolate the method under tests from its dependencies and test it in isolation).
Basically determine with a debugger where the code when under test fails due to the mock passed in and adjust the mock accordingly.
The mock credit card object looks good. I get the error when i assign it to the parent model (I dont get that error when assigning an actual cridt card object). I think i'll just try and mock the method of the existing credit card instead.
This is perfectly fine, too, because you have a direct control over the CreditCard instance that the Transaction instance uses.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.