Top five things to improve your coding (in no particular order):

1. Document your code (automatically)
2. Write Object Oriented code
3. Get acquainted with useful packages
4. Write tests for your code (This post)
5. Use a version control system

As a beginner my usual method for testing my scripts and programs was to either run a few lines using the interactive python shell or simply execute the code and see if I got any errors. In one sense this helped me to understand how certain aspects of python worked and get quick feedback, but often I would just be finding lots of silly syntax errors, like forgetting a semicolon or not indenting correctly.

After I started some freelancing projects which required a little bit more effort to deploy the code, digging through the error logs to find the stack trace each time for such small errors quickly made me try to find a solution to catch such simple bugs before they could even be committed to a repo or deployed for testing.

Code Syntax and Style checking with Pylint

My testing package of choice is pylint. It’s a simple as running `pylint filename.py`, and a detailed report on your code quality is produced. It checks for errors in the syntax and formatting of your code, although does not test the running of the code itself – but this is an important first step. Pylint also checks your code against PEP8 which is a standard style guide for Python – you don’t have to comply with this for your code to work – however writing according to PEP8 will make your code much easier to read and maintain and less likely to contain mistakes, so its a good to aim for this when writing Python professionally. Here’s a sample of the output of pylint (note these are all PEP8 non-compliance rather than errors as such):

C:610, 0: Trailing whitespace (trailing-whitespace)
C:612, 0: Exactly one space required around assignment
        reg_date=datetime.now()
                ^ (bad-whitespace)
W:613, 0: Found indentation with tabs instead of spaces (mixed-indentation)
C:787, 0: Unnecessary parens after 'if' keyword (superfluous-parens)
C:  1, 0: Missing module docstring (missing-docstring)
C:  4, 0: Multiple imports on one line (base64, jinja2, qrcode, zipfile, os) (multiple-imports)

I’ve just been running pylint manually, however you could automate this into your workflow by running pylint on a git hook (we’ll look at version control using git in part 5), basically you could enforce that you code can only be committed to your version control repository if it successfully passes the pylint test.

I’m tending to diable warnings / errors generated by pylint for lines too long (C0301) and mutliple statements on one line (C0321), as I don’t mind about that. Run the command like this:

pylint my_code.py --disable=C0301,C0321

Unittest

I have known about Unittest for a while however I’ve never used it in anger. Part of the reason for me writing this blog post was to force me to try it out so I can be up to speed for my next project. So I picked a project and decided to write some unit tests. The first project I chose was my python API wrapper for stannp (a postcard sending service)…. until I realised that basically every single function made a call to the web API, and therefore it felt like each test I wrote would be mostly testing the API was working rather than my code. So I started looking for another project… until I realised almost every single function I’ve written somehow interacts with an external online service, and I can’t simply write an test to assert whether or not an expected condition is true or not. Finally I looked at my weather monitoring app but half of the functions either required access to the Raspberry Pi GPIO with connected hardware, or connection to the internet to upload to thingspeak… I could only find one function which was purely maths / logic and didn’t require connection to some external service.

So I had a go at writing a test, basically doing a sample calculation by hand (to 11 decimal places) and using this as a test case, and rejecting negative numbers:

import unittest
from air_quality import sensor

class TestCases(unittest.TestCase):

    def test_pcs_to_ugm3(self):
        """Does conversion of concentration of PM2.5 particles per 0.01 cubic feet to µg/ metre return a correct value?"""
        self.assertEqual(round(sensor.pcs_to_ugm3(None, 5), 11), 0.01039584505)
        self.assertRaises(ValueError, sensor.pcs_to_ugm3, None, -1)

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

First output from the test:

F
======================================================================
FAIL: test_pcs_to_ugm3 (__main__.TestCases)
Does conversion of concentration of PM2.5 particles per 0.01 cubic feet to µg/ metre return a correct value?
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_air_quality.py", line 9, in test_pcs_to_ugm3
    self.assertRaises(ValueError, sensor.pcs_to_ugm3, None, -1)
AssertionError: ValueError not raised by pcs_to_ugm3

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

FAILED (failures=1)

So obviously the benefit of doing this test was that it prompted me to write a check to raise a ValueError if a negative number was passed to this function:

def pcs_to_ugm3(self, concentration_pcf):
    if concentration_pcf < 0:
        raise ValueError('Concentration cannot be a negative number')
    # code continues...

Output from the test after code revision:

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

OK

So I guess that was some benefit, but honestly I’m really questioning the value of unittest in Python if I struggle to find something I can write a test for. During my research I also found an article ‘Why most unit testing is a waste‘ which gives some detail reasons as to his point of view. If you’ve got any comments if I’m missing something please let me know. But for now, my code testing will consist of using pylint, and I’ll keep my eyes peeled for some good examples of using unittest. And maybe I will think harder about raising errors for functions that should only accept certain values.