Using Fudge

Fudging A Web Service

If you’re testing code that uses a Web Service you wouldn’t want to rely on the Internet because it would slow you down. This is a good scenario in which to use mock objects.

Say you have a Twitter bot that looks something like this:

>>> import twython
>>> def post_msg_to_twitter(msg):
...     api = twython.setup(username='kumar303', password='paszword')
...     api.updateStatus(msg)
...
>>>

Since the twython module is tested independently, you can trust that your code will work as long as it calls the right methods. To set this up in Fudge you declare an expectation of how twython should be used like this:

>>> import fudge
>>> fake_setup = fudge.Fake('twython.setup',
...                           expect_call=True).with_args(
...                                                username='kumar303',
...                                                password='paszword')

This says that the setup() method should be called with some specific arguments. Since setup() returns another object for further API calls, you can chain together further expectations:

>>> fake_api = (fake_setup.returns_fake()
...                       .expects('updateStatus')
...                       .with_arg_count(1))
...

Fudge lets you declare expectations as loose or as tight as you want. If you don’t care about the exact arguments, you can leave off the call to fudge.Fake.with_args(). If you don’t care if a method is actually called you can use fudge.Fake.provides() instead of fudge.Fake.expects(). Likewise, fudge.Fake.with_arg_count() can be used when you don’t want to worry about argument values. There are argument inspectors for checking values in other ways.

To activate the declarative mock created above, patch the module temporarily with your fake:

>>> import twython
>>> patched_api = fudge.patch_object(twython, "setup", fake_setup)

Now you can run code against the fake. Begin each test with fudge.clear_calls() to reset call history from previous tests:

>>> fudge.clear_calls()

Run the code you want to test:

>>> post_msg_to_twitter("hey there fellow testing freaks!")

Call fudge.verify() to make sure all expectations were met. You probably want this at the end of your test but not in a tearDown().

>>> fudge.verify()

And, finally, restore the original object for sanity:

>>> patched_api.restore()

A Simple Test Case

The above code could also be written as a test function, compatible with Nose or py.test. To make things easier, you wrap your test in the fudge.with_fakes() decorator to ensure that fudge.clear_calls() and fudge.verify() are executed.

>>> import fudge
>>> @fudge.with_fakes
... @fudge.with_patched_object(twython, "setup", fake_setup)
... def test_post_msg_to_twitter():
...     post_msg_to_twitter("mmm, fudge")
...
>>> test_post_msg_to_twitter()

You can also patch code using the with statement; see fudge.patcher.patched_context().

A unittest.TestCase

You can write the same exact test using a standard unittest.TestCase like this:

>>> import fudge
>>> import unittest
>>> class TestPostMsgToTwitter(unittest.TestCase):
...
...     def setUp(self):
...         fudge.clear_expectations() # from previous tests
...         fake_setup = fudge.Fake('twython.setup',
...                           expect_call=True).with_args(
...                                                username='kumar303',
...                                                password='paszword')
...         fake_api = (fake_setup.returns_fake()
...                             .expects('updateStatus')
...                             .with_arg_count(1))
...         self.patched = fudge.patch_object(twython, "setup", fake_setup)
...
...     @fudge.with_fakes
...     def test_post_msg_to_twitter(self):
...         post_msg_to_twitter("mmm, fudge")
...
...     def tearDown(self):
...         self.patched.restore()
...
>>> test = TestPostMsgToTwitter('test_post_msg_to_twitter')
>>> test.run()

Be sure to apply the decorator fudge.with_fakes() to any test method that might use fake objects. This will ensure that fudge.clear_calls() and fudge.verify() are executed.

Failed Expectations

Since the previous code declared expectations for how the twython module should be used, your test will raise an AssertionError when those expectations are not met. For example:

>>> api = twython.setup(username='kumar303', password='12345')
...
AssertionError: fake:twython.setup(username='kumar303', password='paszword') was called unexpectedly with args (username='kumar303', password='12345')

If your code forgets to call an important method, that would raise an error at verification time:

>>> api = twython.setup(username='kumar303', password='paszword')
>>> fudge.verify()
...
AssertionError: fake:twython.setup.updateStatus() was not called

A lot of effort has gone into the design of Fudge so that it reports the best possible exception messages in your tests. However, if you prefer to declare fakes in the setUp() of your test case (like the previous example) then you can safely call clear_expectations() at the beginning of setUp().

Clearing Expectations

Fudge assumes that when you declare expectations on a Fake, you will use the Fake object in more than one test. For this reason, you’ll need to clear the expectation registry explicitly if you want to start testing with another fake object.

In other words, if one test uses a fake Twitter API but some test later on uses a fake database and has nothing to do with email then you’ll need to clear the Twitter API expectations before testing with the fake database.

>>> fudge.clear_expectations()

This is different from fudge.clear_calls(), which only clears the actual calls made to your objects during a test.

Typically, this would be done at a module level setup() hook supported by Nose or py.test.

Stubs Without Expectations

If you want a fake object where the methods can be called but are not expected to be called, the code is just the same but instead of Fake.expects() you use Fake.provides(). Here is an example of always returning True for the method is_logged_in():

>>> import fudge
>>> auth = fudge.Fake()
>>> user = auth.provides('current_user').returns_fake()
>>> user = user.provides('is_logged_in').returns(True)

>>> def show_secret_word(auth):
...     user = auth.current_user()
...     if user.is_logged_in():
...         print "Bird is the word"
...     else:
...         print "Access denied"
...

>>> fudge.clear_calls()
>>> show_secret_word(auth)
Bird is the word
>>> fudge.verify()

Note that if user.is_logged_in() is not called then no error will be raised.

Replacing A Method

Sometimes returning a static value isn’t good enough, you actually need to run some code. You can do this using Fake.calls() like this:

>>> import fudge
>>> auth = fudge.Fake()
>>> def check_user(username):
...     if username=='bert':
...         print "Bird is the word"
...     else:
...         print "Access denied"
...
>>> auth = auth.provides('show_secret_word_for_user').calls(check_user)
>>> auth.show_secret_word_for_user("bert")
Bird is the word
>>> auth.show_secret_word_for_user("ernie")
Access denied

Fudging A Callable

Sometimes you might only need to replace a single function, not an instance of a class. You can do this with the keyword argument callable=True. For example:

>>> import fudge
>>> login = fudge.Fake(callable=True).with_args("eziekel", "pazzword").returns(True)
>>> @fudge.with_fakes
... @fudge.with_patched_object("auth", "login", login)
... def test_login():
...     import auth
...     logged_in = auth.login("eziekel", "pazzword")
...     if logged_in:
...         print "Welcome!"
...     else:
...         print "Access Denied"
...
>>> test_login()
Welcome!

However, the above test will not raise an error if you forget to call login(). If you want to fudge a callable and declare an expectation that it should be called, use expect_call=True:

>>> login = fudge.Fake('login', expect_call=True).returns(True)
>>> fudge.clear_calls()
>>> remote_user = None
>>> if remote_user:
...     auth.login("joe","sekret")
...
>>> fudge.verify()
...
AssertionError: fake:login() was not called

Cascading Objects

Some objects you might want to work with will support cascading which means each method returns an object. Here is an example of fudging a cascading SQLAlchemy query. Notice that Fake.returns_fake() is used to specify that session.query(User) should return a new object. Notice also that because query() should be iterable, it is set to return a list of fake User objects.

>>> import fudge
>>> session = fudge.Fake('session')
>>> query = session.provides('query').returns_fake()
>>> query = query.provides('order_by').returns(
...             [fudge.Fake('User').has_attr(name='Al', lastname='Capone')]
...         )

>>> from models import User
>>> for instance in session.query(User).order_by(User.id):
...     print instance.name, instance.lastname
...
Al Capone

Multiple Return Values

Let’s say you want to test code that needs to call a function multiple times and get back multiple values. Up until now, you’ve just seen the Fake.returns() method which will return a value infinitely. To change that, call Fake.next_call() to advance the call sequence. Here is an example using a shopping cart scenario:

>>> import fudge
>>> cart = fudge.Fake('cart').provides('add').with_args('book')
>>> cart = cart.returns({'contents': ['book']})
>>> cart = cart.next_call().with_args('dvd').returns({'contents': ['book','dvd']})
>>> cart.add('book')
{'contents': ['book']}
>>> cart.add('dvd')
{'contents': ['book', 'dvd']}
>>> cart.add('monkey')
...
AssertionError: This attribute of fake:cart can only be called 2 time(s).

Expecting A Specific Call Order

You may need to test an object that expects its methods to be called in a specific order. Just preface any calls to fudge.Fake.expects() with fudge.Fake.remember_order() like this:

>>> import fudge
>>> session = (fudge.Fake("session").remember_order()
...                                 .expects("get_count").returns(0)
...                                 .expects("set_count").with_args(5)
...                                 .next_call(for_method="get_count").returns(5))
...
>>> session.get_count()
0
>>> session.set_count(5)
>>> session.get_count()
5
>>> fudge.verify()

A descriptive error is printed if you call things out of order:

>>> fudge.clear_calls()
>>> session.set_count(5)
...
AssertionError: Call #1 was fake:session.set_count(5); Expected: #1 fake:session.get_count()[0], #2 fake:session.set_count(5), #3 fake:session.get_count()[1], end

Working with Arguments

The fudge.Fake.with_args() method optionally allows you to declare expectations of how arguments should be sent to your object. It’s usually sufficient to expect an exact argument value but sometimes you need to use fudge.inspector functions for dynamic values.

Here is a short example:

>>> import fudge
>>> from fudge.inspector import arg
>>> image = fudge.Fake("image").expects("save")
>>> image = image.with_args("JPEG", arg.endswith(".jpg"), resolution=arg.any_value())

This declaration is very flexible; it allow the following arguments to be sent :

>>> image.save("JPEG", "/tmp/unicorns-and-rainbows.jpg", resolution=72)
>>> image.save("JPEG", "/tmp/very-serious-avatar.jpg", resolution=96)

That’s it! See the fudge API for details: