<jupyter_start><jupyter_text>Dr <NAME>            Senior Lecturer Health Data Science            University of Manchester                                               8.0 Testing and handling errors****  About this NotebookThis notebook introduces the concepts of unit testing and error handling which can be used to make your programs more robust to errors and ensure they work as intended.  Learning Objectives:  At the end of this notebook you will be able to:    - Investigate key features of handling errors in Python- Practice writing unit tests to test blocks of code  Table of contents8.1 [Error handling](error)8.2 [Testing](testing) Another useful thing to be able to do when carrying out calculations is to handle any errors that may arise. syntax errors are when you make an error in the writing of your code such as forgetting a bracket or misspelling a keyword. The other type of error is called an exception. This type of error occurs when running the syntactically correct code leads to an error. We saw an example of this with the ZeroDivisionError previously when we tried to divide a value by zero. One thing you can do when you think that you might run into an error by running a section of code is to try running it and then deal with any exceptions.<jupyter_code>a = 8
a / 0<jupyter_output><empty_output><jupyter_text>8.1 Error handling<jupyter_code>try:
    a = 8
    a / 0
except:
    print("Something went wrong")<jupyter_output>Something went wrong<jupyter_text>This prevents the program from just stopping and allows you take other action, i.e. prompt the user to enter a different input. If you know the kind of error you may experience you can state this explicitly rather than having a catch all as above.<jupyter_code>try:
    a = 8
    a / 0
except ZeroDivisionError as error:
    print(error)<jupyter_output>division by zero<jupyter_text>You can also try to run some code, deal with an exception or otherwise run some code and finally do something else. The best way to understand this is with an example:<jupyter_code>try:
    user_input = input("Enter a number: ")
    ans = 8 / int(user_input)
except ZeroDivisionError as error:
    print(error)
else:
    print("The answer was: ", ans)
finally:
    print("Thank you for entering a number")<jupyter_output>Enter a number: 5
The answer was:  1.6
Thank you for entering a number<jupyter_text>Try running the cell above a few times. Enter a number that is not zero, then try it with a zero and see what happens. The finally bit is where you can run any clean up code that is required whatever the outcome. You can also raise your own exceptions based on your own criteria for how you want your program to work. Try running the cell below entering values above and below 18: A prime number is a positive number greater than one with only 2 factors (numbers it can be divided into giving a whole), itself and 1. Below is some code that checks to see if a number is a prime number or not. Try running it several times and see what output you get for some primes (2, 3, 5, 7, 11).<jupyter_code>def check_prime(num):
    num = int(num)
    if num == 1:
        return False
    elif num == 2:
        return True
    else:
        for i in range(2, num):
            if num % i == 0:
                return False
        return True
print("Number is prime?", check_prime(input("Enter a number:")))<jupyter_output>Enter a number:45
Number is prime? False<jupyter_text>Task 1: 1. Run the code above again but this time enter a boolean value (True or False) or a string. What kind of error does it generate?2. Using the exception handling principles above. Add some exception handling to the check_prime() function to deal with a ValueError and add an appropriate message to the user informing them to enter a numerical value.<jupyter_code>def check_prime(num):
    try:
        num = int(num)
        if num == 1:
            return False
        elif num == 2:
            return True
        else:
            for i in range(2, num):
                if num % i == 0:
                    return False
            return True
    except ValueError:
        print("Please enter a numerical input")
print("Number is prime?", check_prime(input("Enter a number:")))<jupyter_output>Enter a number:True
Please enter a numerical input
Number is prime? None<jupyter_text>8.2 Testing There are several modules in Python for testing, such as the unittest module. This allows one to construct a test suite where we can add functions to test for specific sorts of errors. These are called unit tests which involve testing the smallest components of functionality in your software, checking they perform as intended. Note: There are levels of testing above unit test, such as integration testing, system testing and acceptance testing within software testing that test higher levels of the software. These higher levels include such things as how different parts of the software work together with each other and the system as a whole. Let's look at an example based on something we did earlier. If we wanted to build a function to convert degrees in fahrenheit to degrees in celsius we could do something like this:<jupyter_code>def farenheit_to_celsius(f):    
    return (f-32)*5/9
farenheit_to_celsius(107)<jupyter_output><empty_output><jupyter_text>So for unit testing we might want to think of some of the ways that we could "break" the code. One way to do this is to think what would happen if it was used it in an unusual or unexpected way. We ideally want to design our code to be robust against such errors. One thing that might cause an issue is to pass in a variable type that is not a number, say a string or boolean (true/false) value.<jupyter_code>farenheit_to_celsius("Hello world")<jupyter_output><empty_output><jupyter_text>Well that did it. We can see that it has generated a TypeError. Let's write a unit test to check for this.<jupyter_code>import unittest

def farenheit_to_celsius(f):    
    return (f-32)*5/9

class TestFahrenheitToCelsius(unittest.TestCase):            
    def test_types(self):
        self.assertRaises(TypeError, farenheit_to_celsius, "Hello world")<jupyter_output><empty_output><jupyter_text>Here we import the unittest module and make a class called TestFahrenheitToCelsius (we will revisit classes in more detail in the notebook on Object Orientated Programming later in the series) to contain all our functions for testing. Here we have a function called test_types and we use an inbuilt test function called assertRaises to check that we get a TypeError when we call our function and pass in a string. In this case "Hello world". To run this in the notebook we need to add the code below.<jupyter_code>if __name__ == '__main__':
    unittest.main(argv=['first-arg-is-ignored'], exit=False)<jupyter_output>.
----------------------------------------------------------------------
Ran 1 test in 0.002s

OK<jupyter_text>This means that we ran one test (it shows in how many seconds) and that it was OK. This means that passing in a string does indeed raise a TypeError as we saw previously. What if we add a boolean value?<jupyter_code>import unittest

def farenheit_to_celsius(f):    
    return (f-32)*5/9

class TestFahrenheitToCelsius(unittest.TestCase):            
    def test_types(self):
        self.assertRaises(TypeError, farenheit_to_celsius, "Hello world")
        self.assertRaises(TypeError, farenheit_to_celsius, True)
        
if __name__ == '__main__':
    unittest.main(argv=['first-arg-is-ignored'], exit=False)<jupyter_output>F
======================================================================
FAIL: test_types (__main__.TestFahrenheitToCelsius)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "<ipython-input-17-1459895b7dbd>", line 9, in test_types
    self.assertRaises(TypeError, farenheit_to_celsius, True)
AssertionError: TypeError not raised by farenheit_to_celsius

----------------------------------------------------------------------
Ran 1 test in 0.002s

FAILED (failures=1)<jupyter_text>This time the test failed as a TypeError was not generated. We could amend the function to raise a TypeError if it doesn't get a valid input (an int or a float). Note: All tests need to start with test_ for the unittest library to work.<jupyter_code>def farenheit_to_celsius(f):
    if type(f) not in [int, float]:
        raise TypeError("Error, Input must be an int or float")
    return (f-32)*5/9
import unittest

class TestFahrenheitToCelsius(unittest.TestCase):            
    def test_types(self):
        self.assertRaises(TypeError, farenheit_to_celsius, "Hello world")
        self.assertRaises(TypeError, farenheit_to_celsius, True)
        
if __name__ == '__main__':
    unittest.main(argv=['first-arg-is-ignored'], exit=False)<jupyter_output>.
----------------------------------------------------------------------
Ran 1 test in 0.001s

OK<jupyter_text>Again it passed the test. We can add additional tests. For example to check that the function returns the expected output given a known input.<jupyter_code>import unittest

class TestFahrenheitToCelsius(unittest.TestCase):
    def test_conversion(self):
        self.assertAlmostEqual(farenheit_to_celsius(32), 0)
        self.assertAlmostEqual(farenheit_to_celsius(104), 40)
        
    def test_types(self):
        self.assertRaises(TypeError, farenheit_to_celsius, True)
        self.assertRaises(TypeError, farenheit_to_celsius, "A string")
        
if __name__ == '__main__':
    unittest.main(argv=['first-arg-is-ignored'], exit=False)
import unittest

class TestStringMethods(unittest.TestCase):
    def test_upper(self):
        self.assertEqual('foo'.upper(), 'FOO')

if __name__ == '__main__':
    unittest.main(argv=['first-arg-is-ignored'], exit=False)<jupyter_output>...
----------------------------------------------------------------------
Ran 3 tests in 0.004s

OK<jupyter_text>Task 2: Using the code above add a test called test_isupper(self) and using the assertTrue and assertFalse test that 'FOO'.isupper() is True and 'Foo'.isupper() is False.    Hint: if you get stuck have a look at the documentation for the unnittest library<jupyter_code>import unittest

class TestStringMethods(unittest.TestCase):
    def test_upper(self):
        self.assertEqual('foo'.upper(), 'FOO')
    def test_isupper(self):
        self.assertTrue('FOO'.isupper())
        self.assertFalse('Foo'.isupper())
        
if __name__ == '__main__':
    unittest.main(argv=['first-arg-is-ignored'], exit=False)<jupyter_output>....
----------------------------------------------------------------------
Ran 4 tests in 0.005s

OK
{'fraction_non_alphanumeric': 0.10995792426367461, 'fraction_numerical': 0.010098176718092567, 'mean_word_length': 3.254574383452665, 'pattern_counts': {'":': 0, '<': 49, '<?xml version=': 0, '>': 49, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '14', 'dir': 'jupyter-structured-clean-dedup', 'id': '138823', 'license': 'Apache-2.0', 'n_tokens_mistral': 2989, 'n_tokens_neox': 2869, 'n_words': 1287, 'path': 'Intro to Python/Intro to Python Book 8 (testing and error handling).ipynb', 'repo_name': 'HDRUK/hie-training', 'size': '10700'}