Say you have a method that uses Python’s standard smtplib module to send email:
>>> def send_email(recipients, sender, msg):
... import smtplib
... msg = ("From: %s\r\nTo: %s\r\n\r\n%s" % (
... sender, ", ".join(recipients), msg))
... s = smtplib.SMTP()
... s.connect()
... s.sendmail(sender, recipients, msg)
... s.close()
... print "Sent an email to %s" % recipients
...
>>>
You don’t want to send an email each time you run a test but you want to be sure that your code is able to send email. Fudge recommends this strategy: Since you trust that the SMTP class works, expect that your application under test uses the SMTP class correctly. If the application calls the wrong method or forgets to call a method then your test should fail. Here’s how to set it up:
>>> import fudge
>>> from fudge.inspector import arg
>>> SMTP = (fudge.Fake('SMTP')
... .expects('__init__')
... .expects('connect')
... .expects('sendmail').with_args(
... "you@yourhouse.com",
... ["kumar@hishouse.com"],
... arg.contains("To: kumar@hishouse.com"))
... .expects('close'))
...
Next, patch the module temporarily with your fake:
>>> patched_smtplib = fudge.patch_object("smtplib", "SMTP", SMTP)
Now you can test against the fake object. Begin each test with fudge.clear_calls() so that call history is reset:
>>> fudge.clear_calls()
Run the code you want to test:
>>> send_email( ["kumar@hishouse.com"], "you@yourhouse.com",
... "hi, I'm reading about Fudge!")
...
Sent an email to ['kumar@hishouse.com']
Call fudge.verify() to make sure all expectations were met:
>>> fudge.verify()
And, finally, restore your patches:
>>> patched_smtplib.restore()
The above code could also be written as a test function, compatible with Nose or py.test:
>>> import fudge
>>> @fudge.with_fakes
... @fudge.with_patched_object("smtplib", "SMTP", SMTP)
... def test_email():
... send_email( ["kumar@hishouse.com"],
... "you@yourhouse.com",
... "Mmmm, fudge")
...
>>> test_email()
Sent an email to ['kumar@hishouse.com']
You can also patch code using the with statement; see fudge.patcher.patched_context().
With a little more code, you can write the test above using a standard unittest.TestCase like this:
>>> import fudge
>>> import unittest
>>> class TestEmail(unittest.TestCase):
...
... def setUp(self):
... self.patched = fudge.patch_object("smtplib", "SMTP", SMTP)
... fudge.clear_calls()
...
... def tearDown(self):
... self.patched.restore()
...
... @fudge.with_fakes
... def test_email(self):
... send_email( ["kumar@hishouse.com"],
... "you@yourhouse.com",
... "Mmmm, fudge")
...
>>> test = TestEmail('test_email')
>>> test.run()
Sent an email to ['kumar@hishouse.com']
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().
Since the previous code declared expectations for how the sendmail() method should be called, your test will raise an AssertionError when those expectations are not met. For example:
>>> s = SMTP()
>>> s.connect()
>>> s.sendmail("whoops")
...
AssertionError: fake:SMTP.sendmail(...) was called unexpectedly with args ('whoops')
If your code forgets to call an important method, that would be an error too:
>>> s = SMTP()
>>> s.connect()
>>> fudge.verify()
...
AssertionError: fake:SMTP.sendmail(...) was not called
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 SMTP but some test later on uses a fake database and has nothing to do with email then you’ll need to clear the SMTP 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.
If you’re using a test framework like Nose or py.test that supports module level setup / teardown hooks, one strategy is to declare all Fake objects at the top of your test module and clear expectations after all tests are run on your Fake objects. Here is an example of how you could lay out your test module:
>>> import fudge
>>> SMTP = (fudge.Fake()
... .expects('__init__')
... .expects('connect')
... .expects('sendmail').with_arg_count(3)
... .expects('close'))
...
>>> def teardown_module():
... fudge.clear_expectations()
...
>>> @fudge.with_fakes
... @fudge.with_patched_object("smtplib", "SMTP", SMTP)
... def test_email():
... send_email( ["kumar.mcmillan@gmail.com"],
... "you@yourhouse.com",
... "Mmmm, fudge")
...
The above test module will be executed as follows:
>>> try:
... test_email()
... finally:
... teardown_module()
Sent an email to ['kumar.mcmillan@gmail.com']
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.
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
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
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
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).
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
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: