Skip to main content

Python: A lightning quick introduction to virtualenv, nose, mock, monkey patching, dependency injection, and doctest

pip

pip is a tool for installing Python packages. Python 2.7.9 and later include pip by default. If you don't have pip, you'll need to install it.

virtualenv

virtualenv is a tool for installing Python packages locally (i.e. local to a particular project) instead of globally. Here's how to get everything setup:

# Make sure you're using the version of Python and pip that you want to use.
sudo which python
sudo which pip

sudo pip install virtualenv

Now, let's setup a new project:

mkdir ~/Desktop/sfpythontesting
cd ~/Desktop/sfpythontesting
virtualenv env

# Do this anytime you want to work on the application.
. env/bin/activate

# Make sure that pip is running from within the env.
which pip

pip install nose
pip install mock
pip freeze > requirements.txt

# Now that you've created a requirements.txt, other people can just run:
# pip install -r requirements.txt

nose

Nose is a popular Python testing library. It simple and powerful.

Setup the project structure:

mkdir sfpythontesting
touch sfpythontesting/__init__.py
mkdir tests

Create a file, ~/Desktop/sfpythontesting/sfpythontesting/main.py with the following:

import random

def sum(a, b):
  return a + b

Now, create another file, ~/Desktop/sfpythontesting/tests/test_main.py with the following:

from nose.tools import assert_equal, assert_raises
import mock

from sfpythontesting import main

def test_sum():
  assert_equal(main.sum(1, 2), 3)

To run the tests:

nosetests --with-doctest

Testing a function that raises an exception

Add the following to main.py:

def raise_an_exception():
  raise ValueError("This is a ValueError")

And the following to test_main.py:

def test_raise_an_exception():
  with assert_raises(ValueError) as context:
    main.raise_an_exception()
  assert_equal(str(context.exception), "This is a ValueError")

Your tests should still be passing.

Monkeypatching

Sometimes there are parts of your code that are difficult to test because they involve randomness, they are time dependent, or they involve external things such as third-party web services. One approach to solving this problem is to use a mocking library to mock out those sorts of things:

Add the following to main.py:

def make_a_move_with_mock_patch():
  """Figure out what move to make in a hypothetical game.

  Use random.randint in part of the decision making process.

  In order to test this function, you have to use mock.patch to monkeypatch random.randint.

  """
  if random.randint(0, 1) == 0:
    return "Attack!"
  else:
    return "Defend!"

Now, add the following to test_main.py. This code dynamically replaces random.randint with a mock (that is, a fake version) thereby allowing you to make it return the same value every time.

@mock.patch("sfpythontesting.main.random.randint")
def test_make_a_move_with_mock_patch_can_attack(randint_mock):
  randint_mock.return_value = 0
  assert_equal(main.make_a_move_with_mock_patch(), "Attack!")

@mock.patch("sfpythontesting.main.random.randint")
def test_make_a_move_with_mock_patch_can_defend(randint_mock):
  randint_mock.return_value = 1
  assert_equal(main.make_a_move_with_mock_patch(), "Defend!")

Your tests should still be passing.

Here's a link to a more detailed article on the mock library.

Dependency injection

Another approach to this same problem is to use dependency injection. Add the following to main.py:

def make_a_move_with_dependency_injection(randint=random.randint):
  """This is another version of make_a_move.

  Accept the randint *function* as a parameter so that the test code can inject a different
  version of the randint function.

  This is known as dependency injection.

  """
  if randint(0, 1) == 0:
    return "Attack!"
  else:
    return "Defend!"

And add the following to test_main.py. Instead of letting make_a_move_with_dependency_injection use the normal version of randint, we pass in our own special version:

def test_make_a_move_with_dependency_injection_can_attack():
  def randint(a, b): return 0
  assert_equal(main.make_a_move_with_dependency_injection(randint=randint), "Attack!")

def test_make_a_move_with_dependency_injection_can_defend():
  def randint(a, b): return 1
  assert_equal(main.make_a_move_with_dependency_injection(randint=randint), "Defend!")

To learn more about dependency injection in Python, see this talk by Alex Martelli.

Since monkeypatching and dependency injection can solve similar problems, you might be wondering which one to use. This turns out to be sort of a religious argument akin to asking whether you should use Vi or Emacs. Personally, I recommend using a combination of PyCharm and Sublime Text ;)

My take is to use dependency injection when you can, but fall back to monkeypatching when using dependency injection becomes impractical. I also recommend that you not get bent out of shape if someone disagrees with you on this subject ;)

doctest

One benefit of using nose is that it can automatically support a wide range of testing APIs. For instance, it works with the unittest testing API as well as its own testing API. It also supports doctests which are tests embedded inside of the docstrings of normal Python code. Add the following to main.py:

def hello_doctest(name):
  """This is a Hello World function for using Doctest.

  >>> hello_doctest("JJ")
  'Hello, JJ!'

  """
  return "Hello, %s!" % name

Notice the docstring serves as both a useful example as well as an executable test. Doctests have fallen out of favor in the last few years because if you overuse them, they can make your docstrings really ugly. However, if you use them to make sure your usage examples keep working, they can be very helpful.

Conclusion

Ok, there's my lightning quick introduction to virtualenv, nose, mock, monkey patching, dependency injection, and doctest. Obviously I've only just scratched the surface. However, hopefully I've given you enough to get started!

As I mentioned above, people tend to have really strong opinions about the best approaches to testing, so I recommend being pragmatic with your own tests and tolerant of other people's strong opinions on testing. Furthermore, testing is a skill, kind of like coding Python is a skill. To get really good at it, you're going to need to learn a lot more (perhaps by reading a book) and practice. It'll get easier with time.

If you enjoyed this blog post, you might also enjoy my other short blog post, The Zen of Testing. Also, here's a link to the code I used above.

Comments

Marius Gedminas said…
I believe these days get-pip.py is the recommended way of installing pip (and setuptools):
http://www.pip-installer.org/en/latest/installing.html#install-or-upgrade-pip
jjinux said…
Good tip. Thanks!
Thanks for the post! Totally missed "virtualenv"! Will come in very handy today.

Popular posts from this blog

Drawing Sierpinski's Triangle in Minecraft Using Python

In his keynote at PyCon, Eben Upton, the Executive Director of the Rasberry Pi Foundation, mentioned that not only has Minecraft been ported to the Rasberry Pi, but you can even control it with Python. Since four of my kids are avid Minecraft fans, I figured this might be a good time to teach them to program using Python. So I started yesterday with the goal of programming something cool for Minecraft and then showing it off at the San Francisco Python Meetup in the evening.

The first problem that I faced was that I didn't have a Rasberry Pi. You can't hack Minecraft by just installing the Minecraft client. Speaking of which, I didn't have the Minecraft client installed either ;) My kids always play it on their Nexus 7s. I found an open source Minecraft server called Bukkit that "provides the means to extend the popular Minecraft multiplayer server." Then I found a plugin called RaspberryJuice that implements a subset of the Minecraft Pi modding API for Bukkit s…

Apple: iPad and Emacs

Someone asked my boss's buddy Art Medlar if he was going to buy an iPad. He said, "I figure as soon as it runs Emacs, that will be the sign to buy." I think he was just trying to be funny, but his statement is actually fairly profound.

It's well known that submitting iPhone and iPad applications for sale on Apple's store is a huge pain--even if they're free and open source. Apple is acting as a gatekeeper for what is and isn't allowed on your device. I heard that Apple would never allow a scripting language to be installed on your iPad because it would allow end users to run code that they hadn't verified. (I don't have a reference for this, but if you do, please post it below.) Emacs is mostly written in Emacs Lisp. Per Apple's policy, I don't think it'll ever be possible to run Emacs on the iPad.

Emacs was written by Richard Stallman, and it practically defines the Free Software movement (in a manner of speaking at least). Stal…

ERNOS: Erlang Networked Operating System

I've been reading Dreaming in Code lately, and I really like it. If you're not a dreamer, you may safely skip the rest of this post ;)

In Chapter 10, "Engineers and Artists", Alan Kay, John Backus, and Jaron Lanier really got me thinking. I've also been thinking a lot about Minix 3, Erlang, and the original Lisp machine. The ideas are beginning to synthesize into something cohesive--more than just the sum of their parts.

Now, I'm sure that many of these ideas have already been envisioned within Tunes.org, LLVM, Microsoft's Singularity project, or in some other place that I haven't managed to discover or fully read, but I'm going to blog them anyway.

Rather than wax philosophical, let me just dump out some ideas:Start with Minix 3. It's a new microkernel, and it's meant for real use, unlike the original Minix. "This new OS is extremely small, with the part that runs in kernel mode under 4000 lines of executable code." I bet it&…