Skip to main content

How to Write Python Unit Tests

Tags

How to Write Python Unit Tests

So there you are, you just finished the last lines of code needed to complete a ticket on one of the big features of your application.  Now what?  Submit a pull request and have your team review it? Run your code through a linter to check for conventions?  Manually test the new feature for validity?  The answer is, yes to all of these ideas, but one thing is still missing. You still need to unit test your new feature to manually validate that the code in your new feature works.  What does that do for you?  Unit testing validates that given a bit of code, a desired output is achieved as a result of executing said code.  Unit testing, unlike functional or system testing, is very black and white in nature and is a way to test the output of your code more than anything.  Unit tests usually pass or fail and there is no gray area, but unit testing is an excellent technique to validate your code generates a targeted output.  And that is why I wanted to write this tutorial, to demonstrate how unit tests works in Python, how easy it is to get started writing unit tests in your project, and then provide an example of a simple unit test as to illustrate how this works in Python.

 

NOTE:  This tutorial is not aimed at providing an exhaustive look at all unit testing scenarios in Python, but simply how to get up and running writing unit tests.  The code in this tutorial is aimed at users who have some previous knowledge of Python, but do not have to be an advanced users by any means. The code in this tutorial has been tested against Python 2.7 and Python 3.7 to ensure usability across different versions of the language.  I hope you enjoy!

 

Getting Started 🚀

To get started we need a bit of code functionality to test! As mentioned previously, we are testing the input and output of our code and not manually or functionally testing.  As an example, I created two Python functions that are nearly identical.  They both take two inputs; the first is a list of keywords and the second is a text based log file.  Both functions are designed to count up the number of occurrences in a log file and return the count as an numeric integer.  A use case for this would be the needle in a haystack scenario.  The first function, keyword_occurrences_any, uses the Python any function to grab any occurrence of a keyword in a line to increment the occurrence variable.  The keyword_occurrences_for function approaches this problem a bit different.  The for function uses two nested for loops to loop through each line and each word in the line.  That way if there are multiple occurrences on the same line, they all are counted.  The any function would only count one.

Below is an example code for each function.  The output for executing this script is:

 

4 matches found in the log file
5 matches found in the log file

# -*- coding: utf-8 -*-
#
# Python example of two functions used to obtain the same result.
# Unit tests will be created against these functions.
#
 
import os, sys
 
 
def keyword_occurrences_any(keywords_list, logfile_str):
 
	occurrences = 0
	logfile_lines = logfile_str.split('\n')
 
        # Using the any technique will only count 1 per line
	for line in logfile_lines:
		if any(key in line for key in keywords_list):
			occurrences += 1
 
	return occurrences
 
def keyword_occurrences_for(keywords_list, logfile_str):
 
	occurrences = 0
	logfile_lines = logfile_str.split('\n')
 
        # Using the double for technique will get each word
	for line in logfile_lines:
		line_split = line.split(" ")
		for word in line_split:
			if word in keywords_list:
				occurrences += 1
 
	return occurrences
 
 
log_file = """
           systemd[1]: Starting python Network Manager Script Dispatcher Service...
	   systemd[1]: Started Network Manager Script Dispatcher Service.
	   nm-dispatcher: req:1 'dhcp4-change' [enp4s0]: new request (1 scripts)
	   nm-dispatcher: req:1 'dhcp4-change' [enp4s0]: start running ordered scripts...
	   systemd[1]: Starting Clean python session files...
	   systemd[1]: Started Clean python session files.
	   CRON[8283]: (agnosticdev) CMD (cd / && run-parts --report /etc/cron.hourly)
	   """
keywords = ['python', 'error', 'Network', 'failure']
 
matches = keyword_occurrences_any(keywords, log_file)
print("{0} matches found in the log file".format(matches))
 
matches = keyword_occurrences_for(keywords, log_file)
print("{0} matches found in the log file".format(matches))

 

Writing the Unit Test 🐍

Writing Python unittests for the functions demonstrated above can be done using a couple of different methods.  The method I am demonstrating is the way I learned to write unit tests from the source of CPython itself.  To get started I first create a test file, then import the unittest module, next define the testing class, and lastly, define each test case as a new function in the testing class.  Let's take a look at the example I created for test_keyword_occurrences.py.  You can see in the top of this file I have imported the unittest module as well as the functions I need from log_parser.  Next, I created a class to define the tests I am going to create for my keyword_occurrence function.  Lastly, I created a way to kick off my tests by executing the main() function on the unittest object.

Now let's take closer look at my test class.  The first thing you may notice is the setUp and tearDown methods.  These methods are used to setup the log file variables, and delete them once the tests have finished.  Next, take a look at the test_occurrences method.  This method runs a test with the keyword_occurrences_any method to assert the the number of occurrences of a keyword in a log file.  Because of the single occurrence in each line, the test correctly assertEqual with the returned number of occurrences.  Next let's test the output of keyword_occurrences_any against keyword_occurrences_for on the same log file.  This will assert an error due to the way the any function is setup versus the for function and thus our unit test has detected a bug in our code that we can quickly fix.

The output of the test_any_versus_for function will be:

 

======================================================================
FAIL: test_any_versus_for (__main__.KeywordOccurrencesTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_keyword_occurrences.py", line 59, in test_any_versus_for
    self.assertEqual(occurrence_any, occurrence_for)
AssertionError: 4 != 5

---------------------------------------------------------------------

import unittest
from log_parser import keyword_occurrences_any, keyword_occurrences_for
 
 
class KeywordOccurrencesTestCase(unittest.TestCase):
 
	def setUp(self):
		self.log_1   = """
			       systemd[1]: Starting Network Manager Script Dispatcher Service...
			       systemd[1]: Started Network Manager Script Dispatcher Service.
			       systemd[1]: Starting Clean python session files...
			       systemd[1]: Started Clean python session files.
			       """
 
		self.log_2   = """
			       systemd[1]: Starting Daemon Manager Script Dispatcher Service...
			       systemd[1]: Started Daemon Manager Script Dispatcher Service.
			       systemd[1]: Starting Clean php session files...
			       systemd[1]: Started Clean php session files.
			       """
 
		self.log_3   = """
			       systemd[1]: Starting Network python Manager Script Dispatcher Service...
			       systemd[1]: Started Network Manager Script Dispatcher Service.
			       systemd[1]: Starting Clean python session files...
			       systemd[1]: Started Clean python session files.
			       """
 
	def tearDown(self):
		del self.log_1
		del self.log_2
		del self.log_3
 
 
	def test_occurrences(self):
 
		# Test the for one occurrence in each line.
		occurrence_value = keyword_occurrences_any(['python', 'Network'], self.log_1)
 
		self.assertEqual(occurrence_value, 4)
 
		# Test 0 occurrences
		occurrence_value = keyword_occurrences_any(['python', 'Network'], self.log_2)
 
		self.assertEqual(occurrence_value, 0)
 
 
	def test_any_versus_for(self):
		occurrence_any = keyword_occurrences_any(['python', 'Network'], self.log_3)
 
		occurrence_for = keyword_occurrences_for(['python', 'Network'], self.log_3)
 
		# This is expected to fail because the keyword_occurrences_any method only searches by line
                # and returns a boolean if a keyword was found, but not multiple keywords.
                # 
                # While as the keyword_occurrences_for method searches word by word and counts each word.
		# The for technique used a word by word count
		self.assertEqual(occurrence_any, occurrence_for)
 
                # Another technique for a passing test would be to check the inequality.
                # self.assertNotEqual(occurrence_any, occurrence_for)
 
 
if __name__ == "__main__":
    unittest.main()

In Summary ⌛️

In summary I hope you now know a bit more about using Python unit tests in your project and why they are valuable.  In my opinion, all of your mission critical code in your project should be tested, and learning how to use Python unit test is a valuable resource to get that done.  I hope you have enjoyed this tutorial, if you have any questions, comments, or concerns please leave a comment and I will make sure to get back to you as soon as I am able to.

The code for this tutorial can be found on my Github here

Credits: Cover image designed by Freepik.

Member for

3 years 9 months
Matt Eaton

Long time mobile team lead with a love for network engineering, security, IoT, oss, writing, wireless, and mobile.  Avid runner and determined health nut living in the greater Chicagoland area.

Comments

Rik Roos

Sat, 05/26/2018 - 08:26 AM

Hi,

Your article reminds me to really start with unittests, thank you for this.
Question: In last test you expect the two numbers to be different; so if they are different, the test can be considered as passed. Why does "def test_any_versus_for(self):" not use the method " assertNotEqual(first, second, msg=None)"?

Rik, thank you for the feedback.  I certainly appreciate it  In regards to not using assertNotEqual, I was demonstrating in the article testing two values that you may accidentally expect to be the same and have your unit test correct you.  That way you can examine the methods that you are using and see that the results are not the same because of the method technique.

Tammie

Tue, 04/23/2019 - 11:10 AM

Hi there, I believe your website could possibly be having internet browser
compatibility problems. When I look at your website in Safari, it looks fine but when opening
in Internet Explorer, it has some overlapping issues.
I simply wanted to give you a quick heads up!

Besides that, fantastic site!