Testing Python Explained

Python Testing Why Should You Care

A huge shift has occurred in recent years of software development toward testing and ensuring that applications deliver absolute quality. With the advent of social networks and the ever-increasing pressure of media attention, defects in code could be costly to both the individual developer and the company reputation. Whether it be security flaws exposing sensitive customer data, defects that allow hackers access to deface your website, or simply a payment page failing to execute orders, errors can cost your business huge sums of money.

Don’t think of problems on only the large-scale, either. Without a proper testing suite in place, how do you know you have delivered the functionality you set out to deliver at the beginning of writing code? Take a simple data submission form. You have coded the fields to accept a name, address, and email, without any testing. You quickly entered the data as expected and your submit works fine. But what if customers enter something unexpected in the fields? Say for instance a number in the name field. Does the code handle this unwanted values? This kind of thing is what leads to some serious security issues as well as crashes.

You can certainly make great code without tests. The key advantage of writing tests is that testing gives confidence in code before it goes live. In some cases developers are on call to support applications in the middle of the night. Do you want a 2a.m. alert because someone missed an obvious issue after the fact. The other major benefit of testing code is ownership roll over. People come and go and the first person to build something, as time goes on, actually touches it less then the scores of devs that have to wrestle with it down the road.

Fundementals and best practices

Before jumping into the process of writing tests, a good idea is to take some time to get your machine in order and up to date with the tools you will need to proceed. First, make sure you have the correct version of Python installed. Then set up some of the basic tools Python developers use on a daily basis will mean you can easily follow the rest of the steps described here.

Linux

Most Linux distributions come with some version of Python installed. If for some reason you don’t have Python, or perhaps you have an older version, you usually install using your distributions package manager. If Python is not available in this way, then you can visit python.org to download the source and compile it yourself.

Mac

Like Linux, Apple ships a version of Python with every version of OS X. Therefore if you are using a mac you should be all set by default. If you find you need to get Python on your machine for some reason, then you could install a package manager for Mac. Homebrew is lightweight installation, and the install scripts are written in Ruby you can find more information here brew.sh about it.

Windows

Windows is considered a bit out of scope when it comes to this writing as I’m not in the best position to offer advice for it. However, that doesn’t mean the code and advice described are not useful to a Windows user.

Writing Unit Tests

In unit testing, you want to cover the application’s functionality at its most basic level. Then test each individual units of code, typically a method, in isolation to see if given certain conditions it responds in the expected way. Braking testing down to this level gives confidence that each part of the application will behave as expected and enables the coverage of edge cases.

What Should You Test

When first looking into unit testing its natural to ask, what should I test? This is a fair question as applications that are being build nowadays are vast with many complicities. However, unit testing makes the task easier as the whole idea is to focus on the smallest units of code rather than thinking about how to test the large application you are putting together as a whole. Be warned tho as some make tests so focused that they break with data changes and not with functionality changes. Which is what unit testing is really looking to test. So a good rule of thumb to follow is test at the C.R.U.D.(Create. Read. Update. Delete) level and no lower.

Writing Your First Unit Test

I’m sure reaching to this part you are ready to start writing tests. The first example with show you how to structure your test into a class with the correct naming conventions. This will be a great reference area if you need a reminder on how to setup a new structure. The examples following that will be more of the snippets of test methods that will be used inside of a test class.

One of the classic examples for demonstrating unit testing is a small calculator program. Python includes a lot of basic math functionality in the library. This first scenario demonstrates how to implement the calculate class of of a simple calculator program.

class Calculate(object):
    def add(self,x,y):
        return x + y

if __name__ == '__main__':
    calc = Calculate()
    result = calc.add(2,2)
    print(result)

This is a very simple class that is just making use of Python’s built-in math function but we just need something so everyone starts with same footing. Save this code to a file named calculat.py, then execute the code. You should see the end result as follows.

$ python calculate.py
4

Now that we standardized code to look at lets get into writing a test for it. For this we are going to make a file called calculate_test.py, this follows the standard naming conventions of using the class name under test and appending the _test. Save it in a folder called test at the same level as the previous files level. This will be helpful when you have dozens of files that need to be tested in a single repo. Now for the code that goes into calculate_test.py.

import unittest
from app.calculate import Calculate


class TestCalculate(unittest.TestCase):
    def setUp(self):
        self.calc = Calculate()

    def test_add_method_returns_correct_result(self):
        self.assertEqual(4, self.calc.add(2,2))


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

For some this is a lot so lets cover it line-by-line. First imports the functionality you need from Python’s unittest module. We are also importing the class we wrote back in calculate.py, so we can test it’s method. We do this in the setUp method, which is executed before each test, so that we only need to define our instance once and have it created before each test. Then we can write our test and the standard is to append our test name with append_test and explain what the test is doing briefly in the rest of the name. Here we are checking if the add method returns the correct result. We do this by maing use of the assertEqual method provided by the imported unittest module. This checks if the first argument is equal to the second. In the example we just went over we are checking whether 4 is equal to the result of calling the add method in calculate.py with the arguments 2 and 2. In this case everything works out.

$ python test/calculate_test.py
.
------------------------------------------------------
Ran 1 test in 0.001s
OK

Useful Methods in Unit Testing

Below provides a quick guide to many different methods available in the unit test package. For each one, a description of its usage and an example are provided. All methods that take an optional argument, msg=None, can be provided a custom message that is displayed on a failure.

assertEqual(x, y, msg=None)
This method checks to see whether argument x equals argument y. Under the covers, this method is performing the check using the == definition for the objects.

def test_assert_equal(self):
    self.assertEqual(1, 1)

assertAlmostEqual(x, y, places=None, msg=None, delta=None)

On first glance, this method may seem a little strange but in context becomes useful. The method is basically useful around testing calculations when you want a result to be within a certain amount of places to the expected, or within a certain delta.

def test_assert_almost_equal_delta_0_5(self):
    self.assertAlmostEqual(1, 1.2, delta=0.5)

def test_assert_almost_equal_places(self):
    self.asswerAlmostEqual(1, 1.00001, places=4)

Following the PEP-8 standard

As you have been introduced to unit testing in Python, it should be clear that various patterns and standards are followed within the Python community. Some of them are enforced by tools you may wish to use, such as prepending a test name with “test_” to allow runners to keep readability and reuse of code high as it is shared between developers. It helps to give Python code a consistent look and feel that experienced developers are familiar with, and if teams adhere to the accepted standards then when developers move to a new Python project, many aspects of the code should feel familiar.

All Python developers code should conform to the standards outlined within the PEP-8 document. The document is one of the most famous PEPs (Python Enhancement Proposals) not to mention one of the earliest. PEP-8 focuses on the styling of code and puts forward some of the fundamental principles when writing Python code and tests, such as the following.

  • Indents: Four spaces for each indentation
  • Maximum line length: 80 Characters.
  • Blank lines: Two between import, class, and function definitions. One between method definitions inside a class.
  • Import statements: Should be one per line.
  • Class names: Should have capitals for the first letter of each word.
  • Method names: Should use all lowercase and underscores to separate words.

Unit Test Structure

When structuring a project, you can follow some clear standards to make an application’s code more accessible to other Python developers. These simple rules are easy to apply and result in an uniform structure to make it easy to find the test and code files needed.

  • Unit tests should be placed under a test/unit directory at the top level of a project folder.
  • All folders within the application’s code should be mirrored by test folders under test/unit, which will have the units tests for each file in them. For example, app/data should have a mirrored folder of test/unit/app/data.
  • All unit test files should mirror the name of the file they are testing, with _test as the suffix. For example, app/data/data_interface.py should have a test file of test/unit/app/data/data_interface_test.py.

Going further

A lot has been covered here but it is by no means an exhaustive description on Python testing. Thing to look into from hear includes TDD (Test Driven Development), Acceptance Testing, writing testable documentation, Automating Testing, and Working on Public code. But for now this is a great place to start getting familiar with Python testing and how you can work it into your projects.