Python Programming Fundamentals 08: Unit Testing and Exception Handling

Original link: https://controlnet.space/2022/07/28/tutorial/python-fund/py-prog-fund-08/

Life is too short, I use Python![1]

This series is a tutorial to help beginners get started with programming. This article mainly introduces unit testing and exception handling in Python.

test

In the software development process, testing is a very important step to ensure the correctness and quality of the program. Errors and deficiencies in programs can also be identified and corrected prior to final deployment.

software-bugs
Fig. 1. Major bugs can have serious consequences. Adapted from [2] and [3]

Some fatal software bugs can have serious consequences, such as the failure of the 737MAX aircraft and the failure of Uber’s autopilot system, as shown in Figure 1 [2, 3]. In addition, some other software bugs that have a serious negative impact on society can be found here ( List_of_software_bugs ). To avoid this, we need to add tests to the program.

The test is divided into 4 levels, namely:

  • Unit testing
    • Individual units or components in the program are tested
    • Make sure every unit is running well without any errors
  • Integration testing
    • Multiple units are combined and tested as a set
    • In integrating interactions between multiple units, errors may be exposed
  • System testing
    • The entire system is tested as a whole
    • Ensure all functions and requirements are met
  • Acceptance testing
    • The complete program is tested before user deployment
    • Assess that the system meets all business requirements

Here we mainly focus on how to write unit tests for Python, other types of tests are part of the software engineering section.

So how do you write tests? The basic idea is to start by thinking about defining a good testing strategy with many different test cases, which is important to ensure the correctness of the program.

Then an excellent testing strategy:

  • To ensure that all functions of the program can be covered and tested in a limited time
  • Consists of reasonable, manageable and maintainable test cases
  • Maximize the likelihood of detecting errors or defects

As an example, consider a program that requires input scores to calculate whether a student has passed an exam.

 1
2
3
4
 if mark >= 50 and mark <= 100 :
grade = "Passed"
else :
grade = "Failed"

Valid (positive) use cases:

  • Based on correct input data
  • Example: 55, 60, 65, …, 85, 90, 95, …

Invalid (negative) use case:

  • Based on incorrect input data
  • For example: -1, 0, 5, …, 45, 49, 101, 200, …

Edge use cases:

  • Some boundary values ​​in valid use cases
  • For example: (49, 50) and (100, 101)

Debug

Debug refers to the process of finding and solving defects or problems in computer programs. When writing a program or testing a bug, you need to debug to solve it.

In Python, there are two basic approaches:

  • print statement
  • assert statement

We are already familiar with print , which outputs the value of this variable, which is convenient for observing the value of a variable at runtime.

As for assert :

  • It checks whether an expression is true, and if not, throws an AssertionError exception
  • Syntax: assert (condition), "<error_message>"

for example:

 1
 assert size <= 5 , "size should not exceed 5"

But in short, the best debugging is to actually understand the program you wrote.

Unit Testing in Python

In Python, we need to use the unittest standard library for testing.

  • Create a test class by inheriting unittest.TestCase
  • Define one or more test methods in the test class

Suppose we have a function product_func defined and we need to write unit tests for it.

 1
2
3
 def product_func ( first_arg, second_arg ):
result = first_arg * second_arg
return result

Then the unit test can be written like this:

 1
2
3
4
5
6
7
8
9
 import unittest

class TestForProduct (unittest.TestCase):

def test_product ( self ):
self.assertEqual(product_func( 2 , 4 ), 8 )

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

When running this test, there will be possibilities: OK, FAIL, ERROR.

If in Jupyter Notebook, we can run the test like this.

 1
2
 suite = unittest.TestLoader().loadTestsFromTestCase(TestForProduct)
unittest.TextTestRunner().run(suite)

In unittest , Python provides the following assert methods:

Method Checks that New in
assertEqual(a, b) a == b
assertNotEqual(a, b) a != b
assertTrue(x) bool(x) is True
assertFalse(x) bool(x) is False
assertIs(a, b) a is b 3.1
assertIsNot(a, b) a is not b 3.1
assertIsNone(x) x is None 3.1
assertIsNotNone(x) x is not None 3.1
assertIn(a, b) a in b 3.1
assertNotIn(a, b) a not in b 3.1
assertIsInstance(a, b) isinstance(a, b) 3.2
assertNotIsInstance(a, b) not isinstance(a, b) 3.2

For a more comprehensive introduction and explanation of unittest , please refer to this page [4].

Errors and exceptions in Python

In programming, errors are generally divided into the following three categories:

  • Syntax errors (Syntax erros)
    • The code is syntactically wrong and the compiler/interpreter cannot understand the code
    • Example in Python: SyntaxError
  • Runtime errors
    • The code encountered an error at runtime and can be handled appropriately
    • Examples in Python: ValueError , TypeError , NameError
  • Logic errors
    • Incorrect implementation of program logic
    • The program runs without error, but the result is wrong

Let’s take some Python examples to illustrate.

SyntaxError :

  • Syntax error in program
 1
2
 if a_number > 2
print (a_number, " is greater than 2 ")

NameError :

  • An undefined variable or module is used in the program
 1
 a_number = random.random()

TypeError :

  • Attempt to use incompatible object types
 1
2
 if a_number > 2 :
print (a_number + " is greater than 2 ")

ValueError :

  • Attempt to pass in a parameter, the type is correct but the value is wrong
 1
 sum_of_two = int ( '1' ) + int ( 'b' )

For more Python error exceptions, please refer to this page [5].

Exception Handling in Python

In Python, the try and except keywords are mainly used to handle exceptions.

try and except :

  • The statement inside the try block will be executed, if no exception occurs, the except block will be skipped
  • If an exception occurs and the conditions in except are met, the corresponding except block will be executed
  • If an exception occurs, but no except condition is met, the program will still report an error and exit

The following is an example. If a non-numeric string is input, or the divisor is 0, it can be processed in the corresponding except code block.

 1
2
3
4
5
6
7
8
9
 try :
num1 = int ( input ("Enter first number: "))
num2 = int ( input ("Enter second number: "))
result = num1 // num2
print ( "Result of division:" , result)
except ValueError:
print ( "Invalid input value" )
except ZeroDivisionError:
print ( "Cannot divide by zero" )

else :

  • If no exception occurs, the else block will be executed
  • This is useful if you need to run some code when no exception occurs

example:

 1
2
3
4
5
6
7
8
9
10
 file_name = "input_file.txt"
try :
file_handle = open (file_name, "r" )
except IOError:
print ( "Cannot open" , file_name)
except RuntimeError:
print ( "A run-time error has occurred" )
else :
print (file_name, "has" , len (file_handle.readlines()), "lines" )
file_handle.close()

finally :

  • as a cleanup block of code
  • A block of code that will be executed whether or not an exception occurs
 1
2
3
4
5
6
7
8
9
10
11
12
 file_name = "input_file.txt"
try :
file_handle = open (file_name, "r" )
except IOError:
print ( "Cannot open" , file_name)
except RuntimeError:
print ( "A run-time error has occurred" )
else :
print (file_name, "has" , len (file_handle.readlines()), "lines" )
file_handle.close()
finally :
print ( "Exiting file reading" )

practice test

In this exercise, let’s try to write code in a way that handles all exceptions, and also needs to write unit tests for the program. If necessary, add comments to aid development.

Debugging

We need to write a program to calculate a student’s GPA. Each student needs to calculate the grades of 5 courses, and the calculation rules are as follows:

 1
2
3
4
5
 A ----- 4.0
B ----- 3.0
C ----- 2.0
D ----- 1.0
F ----- 0.0

There is a buggy code here and we need to fix it.

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 number_of_courses = 3
course_grades = []
total_sum = 0.0

for i in range ( len (number_of_courses)):
input_grades.append = input ( "Please enter the grade for " + (i+ 1 ) + " courses: " )

for grades in input_grades:
if grade == "A" :
total_sum += 4.0
elif grade == "B" :
total_sum += 3.0
elif grade == "B" :
total_sum += 2.0
elif grade == "D" :
total_sum += 1.0
elif grade == "F" :
total_sum += 1.0

print ( "The GPA of a student is: " + (total_sum * number_of_courses))

Let’s find out what’s wrong.

The first is that number_of_courses should be 5 instead of 3. You need to add an assert to check that this variable will not go wrong.

 1
2
3
4
 number_of_courses = 5
assert (number_of_courses == 5 ), "The number of courses should be equal to 5"
course_grades = []
total_sum = 0.0

Then there is a TypeError because len in len len(number_of_courses) needs to accept a collection type instead of a number.

 1
 for i in range (number_of_courses):

Then input_grade.append is a method, which needs to be called instead of assignment.
Second, in the combination of strings, (i+1) needs to be converted to strings, because strings cannot be added to numbers.
And input_grade has a NameError because it is not defined.

So this line would be changed to

 1
 course_grades.append( input ( "Please enter the grade for " + str (i+ 1 ) + " unit: " ))

Next input_grades also has a NameError error and needs to use the correct variable name. The same goes for grade .
Then there is a logic error, the user may enter a lowercase letter, and this will skip the processing of the following code block. should be changed to

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 for grades in course_grades:
if grade.upper() == "A" :
total_sum += 4.0
elif grade.upper() == "B" :
total_sum += 3.0
elif grade.upper() == "C" :
total_sum += 2.0
elif grade.upper() == "D" :
total_sum += 1.0
elif grade.upper() == "F" :
total_sum += 0.0
else :
print_flag = False
print ( "Illegal grade encountered. Program aborted." )
break

if print_flag:
print ( "The GPA of a student is: " + str ( float (total_sum / number_of_courses)))

test

In the previous section, we have written a simple program to calculate a student’s GPA. Think about how tests for the program above should be written to check for potential errors. Let’s try writing some unit tests.

First we put the above program into a function.

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 def calculate_GPA ( grade_list ):
total_sum = 0.0
gpa = 0.0

for grade in grade_list:
if grade.upper() == "A" :
total_sum += 4.0
elif grade.upper() == "B" :
total_sum += 3.0
elif grade.upper() == "C" :
total_sum += 2.0
elif grade.upper() == "D" :
total_sum += 1.0
elif grade.upper() == "F" :
total_sum += 0.0
else :
return - 1

gpa = float (total_sum / len (grade_list))
return gpa

The next step is to write unit tests.

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 import unittest

class TestForGPA (unittest.TestCase):
# valid test case
def test_calculate_GPA_1 ( self ):
self.assertEqual(calculate_GPA([ 'A' , 'A' , 'B' , 'D' ]), 3.0 )

# invalid test case
def test_calculate_GPA_2 ( self ):
self.assertEqual(calculate_GPA([ 'A' , 'A' , 'B' , '1' ]), - 1 )

# boundary test case
def test_calculate_GPA_3 ( self ):
self.assertEqual(calculate_GPA([ 'A' , 'A' , 'A' , 'A' ]), 4.0 )

def test_calculate_GPA_4 ( self ):
self.assertEqual(calculate_GPA([ 'F' , 'F' , 'F' , 'F' ]), 0.0 )

In Jupyter Notebook, we need to run the following code to run the tests.

 1
2
3
 test = TestForGPA()
suite = unittest.TestLoader().loadTestsFromModule(test)
unittest.TextTestRunner().run(suite)

exception handling

Let’s improve the code of practice questions for Python Programming Fundamentals 05 .

Below is the original code.

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
twenty one
 # Open simple_file.txt for reading
# Open output_file.txt for writing
with open ( "simple_file.txt" , "r" ) as input_handle, open ( "output_file.txt" , "w" ) as output_handle:
# Perform some data processing
for line in input_handle:
line = line.strip( "\n" )
line_number = line.split( " " )
if line_number[ 2 ] == "one" :
line_number[ 2 ] = "1"
elif line_number[ 2 ] == "two" :
line_number[ 2 ] = "2"
elif line_number[ 2 ] == "three" :
line_number[ 2 ] = "3"
elif line_number[ 2 ] == "four" :
line_number[ 2 ] = "4"
elif line_number[ 2 ] == "five" :
line_number[ 2 ] = "5"
new_line = line_number[ 0 ] + " " + line_number[ 1 ] + " " + line_number[ 2 ] + "\n"
output_handle.write(new_line)

# Both files will be closed after finishing the with block

The text in simple_file.txt needs to be processed, each line of the file contains three values, the first two are numbers, and the third is an English string representing a number, say “one”. What we need to do is convert the third value to a number and write the result of the entire file to output_file.txt , we only take 1~5 as an example.

 1
2
3
4
 1 2 five
5 7 one
6 9 three
9 8 four

After this study, we can use the try-catch code block to better handle exception errors. The following is a reference example.

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
twenty one
twenty two
twenty three
twenty four
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
 try :
# open simple_file.txt for reading
input_handle = open ( 'simple_file.txt' , 'r' )
# open output_file.txt for writing
output_handle = open ( 'output_file.txt' , 'w' )

except IOError:
print ( "cannot open files" )

except RuntimeError:
print ( "some run-time errors" )

else :
# perform some data processing
for line in input_handle:
line = line.strip( "\n" )
line_tokens = line.split( " " )

if line_tokens[ 2 ] == "one" :
line_tokens[ 2 ] = '1'
elif line_tokens[ 2 ] == "two" :
line_tokens[ 2 ] = '2'
elif line_tokens[ 2 ] == "three" :
line_tokens[ 2 ] = '3'
elif line_tokens[ 2 ] == "four" :
line_tokens[ 2 ] = '4'
elif line_tokens[ 2 ] == "five" :
line_tokens[ 2 ] = '5'

new_line = line_tokens[ 0 ] + " " + line_tokens[ 1 ] + " " + line_tokens[ 2 ] + "\n"
output_handle.write(new_line)

#close both files after processing
input_handle.close()
output_handle.close()

finally :
print ( "Exiting program..." )

Series Summary

So far, the basics of Python programming have all been covered. A total of 8 blogs above cover most of the syntax and programming ideas related to Python basics. I believe that after learning the above 8 articles, you will be able to independently write Python programs without encountering great obstacles. As for the future content, I plan to write a few more post-mortem talks for some advanced Python syntax, which may include type annotations, metaclasses, decorators, functional programming and concurrency and so on.

references

This article is reprinted from: https://controlnet.space/2022/07/28/tutorial/python-fund/py-prog-fund-08/
This site is for inclusion only, and the copyright belongs to the original author.

Leave a Comment