Python and unittest

In this blog post, I will show how to create unit tests to go with some python code using unittest. The project will implement a simple stack using an array—or, more properly, a list.

Create a Project

Start by adding and navigating to your project folder, e.g., exercises. Initialize it with git so you can checkin your changes.

mkdir exercises
cd exercises
git init

Open project folder in your IDE.

Create a file, __init__.py, in exercises. Leave it empty. Save and close it.

Add a folder, src, in the project folder, exercises.
Create a file, __init__.py, in src. Leave it empty. Save and close it.
Create a file, stacks.py. Leave this empty, for now.

Add a folder, tests, in the project folder.
Create an init file, __init__.py.
Add this code:

import sys, os

myPath = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, myPath + '/../src/')

Create a test file, test_stacks.py, and add this code:

import unittest

from stacks import Stack

class StackTestCase(unittest.TestCase):
    def test_is_empty_true(self):
        test_obj = Stack()
        self.assertEqual(True, test_obj.is_empty())

if __name__ == '__main__':
    unittest.main()

Run The Test

python -m unittest discover

This will generate the following:

E
======================================================================
ERROR: tests.test_stacks (unittest.loader.ModuleImportFailure)
----------------------------------------------------------------------
ImportError: Failed to import test module: tests.test_stacks
Traceback (most recent call last):
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/unittest/loader.py", line 254, in _find_tests
    module = self._get_module_from_name(name)
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/unittest/loader.py", line 232, in _get_module_from_name
    __import__(name)
  File "/Users/your_name/src/Closet/exercises/tests/test_stacks.py", line 3, in <module>
    from stacks import Stack
ImportError: No module named stacks

----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (errors=1)

Of course, it fails. stacks does not exist. We have not yet coded it. I wrote the test first to show how extreme you can go with TDD (Test Driven Development). The advantage here is that you expect this type of failure, and you really test each and every step along the way. The first thing to do to get rid of this error is to create the class, Stack.

Add this to ./src/stacks.py:

class Stack(object):
    """docstring for Stack"""
    def __init__(self):
        super(Stack, self).__init__()

Run the test again:

python -m unittest discover

This time, the error is a little different:

E
======================================================================
ERROR: test_is_empty_true (tests.test_stacks.StackTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/your_name/src/Closet/exercises/tests/test_stacks.py", line 8, in test_is_empty_true
    self.assertEqual(True, test_obj.is_empty())
AttributeError: 'Stack' object has no attribute 'is_empty'

----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (errors=1)

Again, a very expected error. Add the method, is_empty() to fix it.

    def is_empty(self):
        return self.stack == []

Run the test and see it work:

.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

Notice the E at the beginning has changed to a dot. A dot indicates a successful test. Also, FAILED (errors=1) has become OK.

Add A Second Test

We are missing a test. We should test for is_empty() is False. By adding this test, we will be forced to implement push() to make it pass.
Here’s the test:

    def test_is_empty_false(self):
        test_obj = Stack()
        test_obj.push('data')
        self.assertEqual(False, test_obj.is_empty())

Here’s the failure:

E.
======================================================================
ERROR: test_is_empty_false (tests.test_stacks.StackTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/ramonaridgewell/src/Closet/Udemy Python Algorithms/exercises/tests/test_stacks.py", line 12, in test_is_empty_false
    test_obj.push('data')
AttributeError: 'Stack' object has no attribute 'push'

----------------------------------------------------------------------
Ran 2 tests in 0.000s

FAILED (errors=1)

Here’s the code to fix it:

    def push(self, data):
        self.stack.append(data)

As expected, the tests both now pass:

..
----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

This is probably a good spot to check in the code. Add a new file, .gitignore in the project directory. Add this:

.DS_Store
*.pyc

This code will tell git to ignore the Desktop Services Store and all the python compiled source files—there is no reason to check these in. Since folders are being committed on this checkin, you can’t see the files with their extensions. Run:

git add .

Then run:

git st

before the checkin to ensure only the files you want to check in are listed. When you are satisfied, run:

git commit -m "initial checkin - added is_empty() and push() to new Stack class"

I’m not going to explain creating a github repo here. If you need to know how to do that, check out this blog post for instructions.

What Comes Next?

Now is a good time to think about what other tests you can add to ensure the current codebase is correct. With only push() and is_empty(), there aren’t many interesting things we can test. We could push on a second item, but we can’t really test anything except that is_empty() is still True. I think adding tests around pop() would be a more interesting and efficient use of our time. Let’s write a test.

    def test_pop_handles_empty_stack(self):
        test_obj = Stack()
        self.assertEqual(None, test_obj.pop())
        self.assertEqual(True, test_obj.is_empty())

I started with this code:

    def pop(self):
        data = self.stack[-1]
        del self.stack[-1]
        return data

but it failed with:

Traceback (most recent call last):
  File "/Users/ramonaridgewell/src/Closet/Udemy Python Algorithms/exercises/tests/test_stacks.py", line 17, in test_pop_removes_item_from_stack
    self.assertEqual(None, test_obj.pop())
  File "/Users/your_name/src/Closet/exercises/tests/../src/stacks.py", line 14, in pop
    data = self.stack[-1]
IndexError: list index out of range

----------------------------------------------------------------------
Ran 3 tests in 0.000s

FAILED (errors=1)

Nice boundary condition test. Put is a is_empty() check to solve the problem and make the test green.

    def pop(self):
        if self.is_empty():
            return None

        data = self.stack[-1]
        del self.stack[-1]
        return data

Let’s add a few more tests:

    def test_pop_pops_1_item_stack(self):
        test_obj = Stack()
        test_data = 'data1'
        test_obj.push(test_data)
        self.assertEqual(test_data, test_obj.pop())
        self.assertEqual(True, test_obj.is_empty())

    def test_pop_in_correct_order_from_2_item_stack(self):
        test_obj = Stack()
        test_data1 = 'data1'
        test_data2 = 'data2'
        test_obj.push(test_data1)
        test_obj.push(test_data2)
        self.assertEqual(test_data2, test_obj.pop())
        self.assertEqual(False, test_obj.is_empty())
        self.assertEqual(test_data1, test_obj.pop())
        self.assertEqual(True, test_obj.is_empty())

These both pass. Check in the code.

Another Method and More Tests

The other typical method on a stack is peek(), which displays the most recent item to be pushed onto the stack (or the top of the stack). It should be very similar to pop() except it won’t remove the item from the stack, simply return the value. Here are three tests to go with it:

    def test_peek_handles_empty_stack(self):
        test_obj = Stack()
        self.assertEqual(None, test_obj.peek())
        self.assertEqual(True, test_obj.is_empty())

    def test_peek_shows_item_in_1_item_stack(self):
        test_obj = Stack()
        test_data = 'data1'
        test_obj.push(test_data)
        self.assertEqual(test_data, test_obj.peek())
        self.assertEqual(False, test_obj.is_empty())

    def test_peek_shows_top_of_2_item_stack(self):
        test_obj = Stack()
        test_data1 = 'data1'
        test_data2 = 'data2'
        test_obj.push(test_data1)
        test_obj.push(test_data2)
        self.assertEqual(test_data2, test_obj.peek())
        self.assertEqual(False, test_obj.is_empty())
        self.assertEqual(test_data2, test_obj.peek())

and the new method:

    def peek(self):
        if self.is_empty():
            return None

        return self.stack[-1]

That’s it for peek(). Time to check in the code.

One Last Method

To round out our stack, we should add a size() method. Add the tests:

    def test_size_handles_empty_stack(self):
        test_obj = Stack()
        self.assertEqual(0, test_obj.size())
        self.assertEqual(True, test_obj.is_empty())

    def test_size_returns_1_for_1_item_stack(self):
        test_obj = Stack()
        test_data = 'data1'
        test_obj.push(test_data)
        self.assertEqual(1, test_obj.size())
        self.assertEqual(False, test_obj.is_empty())

    def test_size_returns_correct_number_for_many_item_stack(self):
        test_obj = Stack()
        test_data0 = 'data0'
        test_data1 = 'data1'
        test_data2 = 'data2'
        test_data3 = 'data3'
        test_data4 = 'data4'
        test_data5 = 'data5'
        test_data6 = 'data6'
        test_data7 = 'data7'
        test_data8 = 'data8'
        test_data9 = 'data9'
        test_obj.push(test_data0)
        test_obj.push(test_data1)
        test_obj.push(test_data2)
        test_obj.push(test_data3)
        test_obj.push(test_data4)
        test_obj.push(test_data5)
        test_obj.push(test_data6)
        test_obj.push(test_data7)
        test_obj.push(test_data8)
        test_obj.push(test_data9)
        self.assertEqual(10, test_obj.size())

and the method:

    def size(self):
        return len(self.stack)

Be aware of what you might be missing when you review your test cases. It was interesting to me that we didn’t write any specific tests for push()—it can’t really be tested all by itself. is_empty(), pop() and peek() tests all utilize push(). I believe it has been completely tested in this suite of tests.

That’s it for our little stack. Check in your code and let’s have a beer.

In Conclusion

This is really simple example. I hope you can find some value in learning to unit test as you write your code. As is usually the case, there is way more test code than production code. There always is. The more complex the code, the larger the ratio will become. Tests give developers the confidence to change their code, and know they haven’t broken anything in the process. Tests are your friends—I guarantee it. When I choose not to write tests—and, for me, it’s a carefully considered choice—I always seem to regret it later. I write bugs—everybody does. Tests help ensure there are fewer of them.

You can find this code in my repository, python-stack-w-unittest.

Copyright ©2014-17 Ramona Ridgewell. All rights reserved.

Advertisements
This entry was posted in #Education, Coding, GitHub, Programming, Python, Science, STEM and tagged , , , , , , , , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s