Skip to content

Guide: Writing unit tests Part 1

Aron Fyodor Asor edited this page Mar 24, 2014 · 4 revisions

Preliminaries

First, read up on Unit testing for Python. See this link here. Note that this document is not a comprehensive overview of testing in Python, but is simply a walkthrough on how to begin writing tests for KA Lite with the project's bundled libraries. Check the given link for a more comprehensive overview of testing.

Writing unit tests for already-existing functions

Unfortunately, the KA Lite project did not start with a strong testing culture. But hey, we're here to change that!

Basic python functions

For this tutorial, let's start with a basic function in the fle_utils package:

Here's our function, found in fle_utils/platforms.py:

def system_script_extension(system=None):
    """
    The extension for the one script that could be considered "the os script" for the given system..
    """
    exts = {
        "windows": ".bat",
        "darwin": ".command",
        "linux": ".sh",
    }
    system = system or platform.system()
    return exts.get(system.lower(), ".sh")

Simple to understand, simple to test. Let's get to it.

Testing the happy path a.k.a when everything works perfectly

To get ourselves motivated, let's first test "The Happy Path." This is when the user of this function uses it properly and gives the input our function needs to properly do its job. In this case, the happy path is pretty easy to see: our function can work with no input and should almost always return a proper response!

Preparing our file

Let's get to writing tests for it then. First off, make sure you have a clone of the fle-utils project on your local machine and that you are running Python 2.7. After that, cd into your fle-utils directory and then run pip install -r requirements.txt to install all required files into your system. Then navigate to the testing directory and make sure you testing/platforms_tests.py exists with these lines in it:

import os
import sys
import unittest

sys.path += [os.path.realpath('..'), os.path.realpath('.')]

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

If it already exists and has these lines, then someone already followed this tutorial :) If so, for the sake of this tutorial, feel free to delete the contents of this file and insert those lines instead. Just remember NOT TO COMMIT IT AFTER. I REPEAT, DO NOT COMMIT IT, OR YOUR PULL REQUESTS WILL BE REJECTED TO THE HIGH HEAVENS.

Introducing the mock library

One of the libraries installed in your system when you ran pip install was the mock library. Mock allows us to reliably dictate the output of certain functions, to make tests both reproducible across systems and make them run faster. Check out the full mock documentation. It's a really awesome library.

Writing our first test

Alright, let's start writing our tests! Let's start by writing the test class (insert between from platforms import system_script_extension and if __name__ == '__main__' lines)

class SystemScriptExtensionTests(unittest.TestCase):
    pass

That's the beginnings of our test class, which is empty for now. Next, let's ask ourselves what do we want to test. Looking at the function, let's first test if it will return .bat for windows machines!

class SystemScriptExtensionTests(unittest.TestCase):

    def test_returns_bat_on_windows(self):
        pass

The function name can be anything as long as it starts with test. We then fill out an assert statement to test if it's returning the right extension:

class SystemScriptExtensionTests(unittest.TestCase):

    def test_returns_bat_on_windows(self):
        self.assertEquals(system_script_extension(), '.bat')
        self.assertEquals(system_script_extension('Windows'), '.bat')

We can run it within the fle_utils dir with the command python testing/platforms_test.py. I get an output like this:

F
======================================================================
FAIL: test_returns_bat_on_windows (__main__.SystemScriptExtensionTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "testing/platforms_test.py", line 49, in test_returns_bat_on_windows
    self.assertEquals(system_script_extension(), '.bat')
AssertionError: '.sh' != '.bat'

----------------------------------------------------------------------
Ran 1 test in 0.003s

FAILED (failures=1)

Oh no! The first assertion will only succeed if we're running the test on a Windows machine! Now, we want to have this test be portable across different OSes, and not just succeed on the Windows platform. This is where the mock library comes in: we want to basically mock and hijack the platform.system() function to always return Windows for the scope of this function.

I won't go into the details of how to use the mock library (check it out here), so i'll just throw in the end result:

class SystemScriptExtensionTests(unittest.TestCase):

    @patch.object(platform, 'system')
    def test_returns_bat_on_windows(self, system_method):
        system_method.return_value = 'Windows'

        self.assertEquals(system_script_extension(), '.bat')
        self.assertEquals(system_script_extension('Windows'), '.bat')

Running that, we get this output:

.
----------------------------------------------------------------------
Ran 1 test in 0.001s

OK

Nice! As you can see, we "patch" the system function to always have a return_value of 'Windows'. With that, our function succeeds, and all is well! Tests for the other OSes plus our default return value of .sh should be pretty easy to formulate now based on how we did this test for Windows. Can you try doing them? After you've done so, you can compare your answers to the ones I've written way down below: * * * * * * * * * * * *

Your class should now look similar to this:

class SystemScriptExtensionTests(unittest.TestCase):

    @patch.object(platform, 'system')
    def test_returns_bat_on_windows(self, system_method):
        system_method.return_value = 'Windows'

        self.assertEquals(system_script_extension(), '.bat')
        self.assertEquals(system_script_extension('Windows'), '.bat')

    @patch.object(platform, 'system')
    def test_returns_command_on_darwin(self, system_method):
        system_method.return_value = 'Darwin' # the Mac kernel

        self.assertEquals(system_script_extension(), '.command')
        self.assertEquals(system_script_extension('Darwin'), '.command')

    @patch.object(platform, 'system')
    def test_returns_sh_on_linux(self, system_method):
        system_method.return_value = 'Linux'

        self.assertEquals(system_script_extension(), '.sh')
        self.assertEquals(system_script_extension('Linux'), '.sh')

    @patch.object(platform, 'system')
    def test_returns_sh_by_default(self, system_method):
        system_method.return_value = 'Random OS'

        self.assertEquals(system_script_extension(), '.sh')
        self.assertEquals(system_script_extension('Random OS'), '.sh')

If you run the file with that command, you should get this output:

....
----------------------------------------------------------------------
Ran 4 tests in 0.002s

OK

And all is well in unittest-land. To make sure your tests are working perfectly, try to change the outputs of system_specific_extension a bit. Add a few letters here and there. You'll find that our tests will fail. We are now a little confident about this function 👍

Here's a summary of the things we learned so far:

  • We can mock functions and methods using the patch.object of the mock library
  • We can use self.assertEquals to test if two things are equal. If not, then the test fails.

Before we end this basic tutorial, here's what your testing/platforms_test.py should look like once you've completed this:

import os
import platform
import sys
import unittest

import mock
from mock import patch

sys.path += [os.path.realpath('..'), os.path.realpath('.')]

from platforms import system_script_extension

class SystemScriptExtensionTests(unittest.TestCase):

    @patch.object(platform, 'system')
    def test_returns_bat_on_windows(self, system_method):
        system_method.return_value = 'Windows'

        self.assertEquals(system_script_extension(), '.bat')
        self.assertEquals(system_script_extension('Windows'), '.bat')

    @patch.object(platform, 'system')
    def test_returns_command_on_darwin(self, system_method):
        system_method.return_value = 'Darwin' # the Mac kernel

        self.assertEquals(system_script_extension(), '.command')
        self.assertEquals(system_script_extension('Darwin'), '.command')

    @patch.object(platform, 'system')
    def test_returns_sh_on_linux(self, system_method):
        system_method.return_value = 'Linux'

        self.assertEquals(system_script_extension(), '.sh')
        self.assertEquals(system_script_extension('Linux'), '.sh')

    @patch.object(platform, 'system')
    def test_returns_sh_by_default(self, system_method):
        system_method.return_value = 'Random OS'

        self.assertEquals(system_script_extension(), '.sh')
        self.assertEquals(system_script_extension('Random OS'), '.sh')


if __name__ == '__main__':
    unittest.main()
Clone this wiki locally