Python Unit Testing Example

Russell Bateman
July 2016
last update:

Here's a complete, if small, example of unit testing in Python without any mocking. (Python unit testing with mocking is a different and more complicated demonstration.) The haters will say my Python code doesn't look like Python. Like I care. The world of naming and formatting has moved on since the C Programming Language that was in vogue back when Python was created. So sue me.

One day, I was writing a utility to validate file formats that will be greatly expanded, but preliminarily, will need to validate that a file (or a string) is valid JSON. As I haven't written much in Python over the last few months (I'm a Java guy), I thought I'd add this to my notes as a basic example before it gets seriously bigger.

json_validate.py

The test subject.

import sys
import json

def main( argv ):
    pass           // (TODO: needs to sort out argument and call validator)

def validateFileAsJson( filepath=None ):
    try:
        with open( filepath, 'r' ) as f:
            jsonDict = json.load( f )
    except Exception as e:
        return False
    return True

def validateStringAsJson( string=None ):
    try:
        jsonDict = json.loads( string )
    except Exception as e:
        return False
    return True

if __name__ == "__main__":
    if len( sys.argv ) <= 1:
        sys.exit( main( '--help' ) )
    elif len( sys.argv ) >= 1:
        sys.exit( main( sys.argv ) )
test_json_validate.py

The unit-test code—why you're even looking at these notes).

import sys
import unittest
import json_validate
import testutilities

PRINT = True            # change to False when finished debugging...

GOOD_JSON = '{ "json" : "This is a JSON" }'
BAD_JSON  = '<?xml version="1.0" encoding="UTF-8" standalone="no" ?><json> This isn\'t JSON </json>'

class JsonValidateTest( unittest.TestCase ):
    """
    Test json_validate.py. Use https://jsonformatter.curiousconcept.com/#jsonformatter to
    create suitable (valid) and invalid JSON fodder.
    """
    @classmethod
    def setUpClass( JsonValidateTest ):
        testutilities.turnOnPrinting( PRINT )

    def setUp( self ):
        pass

    def tearDown( self ):
        pass

    def testGoodFileAsJson( self ):
        testutilities.printTestCaseName( sys._getframe().f_code.co_name )
        temp = testutilities.createTemporaryFile( '.json', GOOD_JSON )
        result = json_validate.validateFileAsJson( temp )
        testutilities.eraseTemporaryFile( temp )
        self.assertTrue( result )

    def testBadFileAsJson( self ):
        testutilities.printTestCaseName( sys._getframe().f_code.co_name )
        temp = testutilities.createTemporaryFile( '.json', BAD_JSON )
        result = json_validate.validateFileAsJson( temp )
        testutilities.eraseTemporaryFile( temp )
        self.assertFalse( result )

    def testGoodStringAsJson( self ):
        testutilities.printTestCaseName( sys._getframe().f_code.co_name )
        result = json_validate.validateStringAsJson( GOOD_JSON )
        self.assertTrue( result )

    def testBadStringAsJson( self ):
        testutilities.printTestCaseName( sys._getframe( ).f_code.co_name )
        result = json_validate.validateStringAsJson( BAD_JSON )
        self.assertFalse( result )

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

Some test utilities I like to carry around. (This is a simplified subset.)

import os
import sys
import tempfile

PRINT = False

def spinCommandLine( scriptName=None, commandLine=None ):
    """ This will turn test with spaces into a command line as if sys.argv. """
    sys_argv = []
    if not commandLine:
        return sys_argv
    sys_argv.append( scriptName )
    sys_argv.extend( commandLine.split( ' ' ) )
    return sys_argv

def createTemporaryFile( extension=None, contents=None ):
    if extension:
        (fd, path) = tempfile.mkstemp( suffix=extension )
    else:
        (fd, path) = tempfile.mkstemp( suffix='.tmp' )
    if contents:
        os.write( fd, contents )
    os.close( fd )
    return path

def readTemporaryFileAsString( path=None ):
    if not path:
        return ''
    string = ''
    try:
        with open( path, 'r' ) as f:
            for line in f:
                string = string + line
    except Exception as e:
        print( 'I/O operation failed on temporary file: %s' % e )

def eraseTemporaryFile( path=None ):
    if not path:
        return
    os.remove( path )

def turnOnPrinting( enable=False ):
    global PRINT
    PRINT = enable

def printOrNot( thing='' ):
    if not PRINT:
        return
    print( thing )

CONSOLE_WIDTH = 80

def printTestCaseName( functionName ):
    """
    Call thus:
    printTestCaseName( sys._getframe().f_code.co_name )
    --helps you sort through unit test output by creating a banner.
    """
    global PRINT
    if not PRINT:
        return
    banner = '\nRunning test case %s ' % functionName
    length = len( banner )
    sys.stdout.write( banner )
    for col in range( length, CONSOLE_WIDTH ):
        sys.stdout.write( '-' )
    sys.stdout.write( '\n' )
    sys.stdout.flush()

Setting up and running this in PyCharm...

  1. Install and launch PyCharm from JetBrains.
  2. Create a new project (File → New Project...).
  3. Create a subdirectory, json_utilities, under this project.
  4. Create a subdirectory, test, under this project.
  5. Copy json_validate.py (above) into the json_utilities subdirectory.
  6. Copy test_json_validate.py (above) into the test subdirectory.
  7. Copy testutilities.py (above) into the test subdirectory.
  8. You should see (project is whatever you called it) what's below in your filesystem and in the Project pane of PyCharm. This is the proper relationship of Python code to its unit test code:
    project
    ├── json_utilities
    │   ├── __init__.py*
    │   └── json_validate.py
    └── test
        ├── __init__.py*
        ├── json_validate_test.py
        └── testutilities.py
    

    * Note: __init__.py indicates to Python that this is a python-package directory. This file is empty (nothing in it).

  9. Assuming no red (no syntax errors, etc.), right-click json_validate_test.py and choose Run (or Debug).

You should see something like this:

Testing started at 3:00 PM ...
/usr/bin/python2.7 /home/russ/dev/pycharm-community-2019.1.1/helpers/pycharm/_jb_unittest_runner.py --target test_json_validate.JsonValidateTest
Launching unittests with arguments python -m unittest test_json_validate.JsonValidateTest in
/home/russ/dev/python-json-xml/test

Process finished with exit code 0

Running test case testBadFileAsJson -------------------------------------------

Running test case testBadStringAsJson -----------------------------------------

Running test case testGoodFileAsJson ------------------------------------------


Ran 4 tests in 0.004s

OK

Running test case testGoodStringAsJson ----------------------------------------

From the command line...

Because we've set up our project and test code as it should be, we can use Python's auto-discovery mechanism to run (all) the tests we have written.

$ python -m unittest discover -v
testBadFileAsJson (test.test_json_validate.JsonValidateTest) ...
Running test case testBadFileAsJson -------------------------------------------
ok
testBadStringAsJson (test.test_json_validate.JsonValidateTest) ...
Running test case testBadStringAsJson -----------------------------------------
ok
testGoodFileAsJson (test.test_json_validate.JsonValidateTest) ...
Running test case testGoodFileAsJson ------------------------------------------
ok
testGoodStringAsJson (test.test_json_validate.JsonValidateTest) ...
Running test case testGoodStringAsJson ----------------------------------------
ok

----------------------------------------------------------------------
Ran 4 tests in 0.001s

OK