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...
- Install and launch PyCharm from JetBrains.
- Create a new project (File → New Project...).
- Create a subdirectory, json_utilities, under this project.
- Create a subdirectory, test, under this project.
- Copy json_validate.py (above) into the json_utilities subdirectory.
- Copy test_json_validate.py (above) into the test subdirectory.
- Copy testutilities.py (above) into the test subdirectory.
- 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).
- 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