Python Testing with nose for beginners

I've been meaning to write a simple primer for unit testing in python ever since I realized how invaluable writing tests are. You must be thinking that everyone knows to test they're code. I did too, but I didn't do it. It seemed hard and I was lazy. Fortunately, its really easy to get started.

This blog post is about how to get started with nose to write unit tests for your python code with as little friction as possible.

Motivation

If you have no experience writing tests, I fully empathize with you. I previously only wrote tests when it was 'necessary', which meant I never wrote them. Consequently, during my internships, I found out how little experience I had writing tests and testable code. However, once I was over the initial hurdle, it was easy to see the benefits and flexibility that having tests affords you.

I figured as with most things, this is a skill that I could get better at by doing repeatedly. I started writing tests for most of my code, even one-off class assignments and projects that didn't really care too much about them.

Installation

The setup is really easy. Just pip install nose. (easy_install works too).

pip install nose

Nosetests

The great thing about nose, is how easy it is to write and run tests. Installing nose adds a nosetests command that you can invoke to run your tests inside the folder you're currently at.

By default, nosetests finds:

  • files or folders that contain /[Tt]est/ in their name, eg. model_test.py, modelTest.py etc.
  • within these files, functions and classes with /[Tt]est/ in their name will be run as tests. eg. test_function_simple, test_function_zero etc.

Test files and writing your first test

Where you put your tests is a matter of preference, I prefer to have my tests in the same place as my files for ease of locating them and importing the code to be tested.

If I have /path/to/project/src/model.py, I would have a corresponding /path/to/project/src/test_model.py. So all tests for model.py will go into test_model.py.

Inside your test files, you simply import the code you're testing and test it. I'll be using examples from one of my class assignments so feel free to refer to it for the full source.

Here is an example:

import model


def test_model_total_count():
    m = model.Model()
    assert m.total_count() == 0

    m.incr_gram_count(('h',))
    m.incr_gram_count(('e',))
    m.incr_gram_count(('l',))
    m.incr_gram_count(('l',))
    m.incr_gram_count(('o',))

    assert m.total_count() == (5 + 4)
    assert m.total_count(include_smoothing=False) == 5

In the above example, I import the model file and test the total_count method of the Model object before and after incrementing the 'grams' in the model.

Saving this file and running nosetests on the command line will run test_model_total_count!

Some other things about nose

If you've written tests before, or at least heard of them, you'll know that the example above is really simple and you probably will need a couple more things to get going.

setup and teardown

Chances are, you're writing a couple of tests in each test file that are highly related. nose makes it really easy to write setup and teardown functions for your tests:

def setup():
    # setup here..
    pass

def teardown():
    # teardown here..
    pass

Simply name them as such and the test runner will run the functions before and after each test. (There are other acceptable variants, which I'm leaving out that will be run by the test runner).

assert equals

In the example above, I used the built-in assert function in python. However, when the test fails, the error message isn't really helpful, it'll say that it expected True but got False instead.

nose provides a nice nose.tools._eq_ that takes two values and compares them using the == operator. Upon failure, it gives a nice message, something like "expected 5 but was given 4", helping you to indentify and fix the source of the broken test quickly.

Other bells and whistles

For the sake of making this as simple as possible, I have glossed over many of nose's functionalities. You probably will use some of them eventually but they are not necessary to start writing simple tests and get into the habit of writing testable code. Trust me, it gets easier.

Stuff like:

  • Using a test class instead of test functions
  • Specific setup and teardown functions for specific test functions
  • Running only some tests (and not all of them)
  • Testing that an exception was raised in a test
  • ...

Closing thoughts

Hopefully you feel like writing unit tests in python is really simple after reading this. If you find yourself in the need for mocking and stubing objects in your tests, I highly recommend the mockito-python library that is model after the java library of the same name. It integrates seamlessly with nose and is really intuitive to use.

Happy testing!