<jupyter_start><jupyter_text>![logo2_cropped.jpg](attachment:logo2_cropped.jpg)  Unit Testing A critical part of the SDLC (software development lifecycle), and also best-practice for any form of programming, testing is what allows us to ensure that our code will run correctly.In the SDLC we commonly talk about four types of testing:* Unit testing (or component testing): testing a basic unit of code (e.g. a specific function or feature) works correctly.* Integration testing: testing that different units/components of the code work when integrated together. * System testing: testing the whole system/software works.* Acceptance testing: testing that the system meets the user’s requirements.Of these, the type that is more relevant to the work we are doing is unit testing - basically the most granular tests we will do in development. Python has many approaches for performing testing. Some of the better know are _Robot_ (https://pypi.org/project/robotframework/), _pytest_ (https://pypi.org/project/pytest/) and _Nose2_ (https://pypi.org/project/nose2/). For this tutorial, however, we will be using the in-built (and aptly-named) _unittest_ module.  First we need something to test. Our new function will be used to determine what degree classification (e.g. “Pass” or “Distinction”) a student gets based on their average module marks and project marks. We will also expect a parameter that determines what percentage of the final mark is based on modules (versus project). Our function will read like this:<jupyter_code># takes average module marks, project marks, and the % of the
# final mark that is based on the module marks
# E.g. if the course is 180-credits, with 90 credits from modules
# and 90 credits from project, PMA_split = 0.5

def award_classifier(PMA_average, proj_marks, PMA_split):
    pmas = PMA_average * PMA_split
    proj = proj_marks * (1 - PMA_split)
    final = pmas + proj
    if final < 50:
        return "Fail"
    elif final < 60:
        return "Pass"
    elif final < 70:
        return "Merit"
    else:
        return "Distinction"<jupyter_output><empty_output><jupyter_text>If you have got this far there should be nothing in this function that is particularly “new”. However, like any code, it does need testing!The “typical” way to test a function like this is to package it as a callable module we can test from another Notebook or more likely on another machine using Python directly. There are two ways we can do this. One is too add the above code to a new notebook, rename the notebook to “award_classifier”, download as a “.py” file and finally move that file to our working directory. Two is we can move or upload the copy of this file from the Github repo into our working directory as we did with the Python exercises.Having done this, we can now import the unittest module and our custom module:<jupyter_code>import unittest
from award_classifier import *<jupyter_output><empty_output><jupyter_text>_Note we are using "import *" which tells Python to import everything from the file/module._With the module imported we can now design some tests! The unittest module uses an object-orientated approach, as discussed in the previous section, with objects of a test class used to perform the tests. The main code looks like this<jupyter_code># create our testing class (OOP)
class TestOrders(unittest.TestCase): # creating a child class inheriting from TestCase
    # define our tests
    def test_distinction(self):
        # We 'assert' that the value returned by the function matches the text output provided.
        # If it does, the test is passed, otherwise it is failed.
        self.assertEqual(award_classifier(65, 75, 0.5), 'Distinction')

    def test_fail(self):
        self.assertEqual(award_classifier(45, 54, 0.5), 'Fail')

    def test_merit(self):
        self.assertEqual(award_classifier(65, 55, 0.67), 'Merit')

# if the file is ran execute all the above code
if __name__ == '__main__':
    # If ran in Notebooks we need to replace first argument and prevent automatic close down.
    # If ran from standard Python "unittest.main()" instead
    unittest.main(argv=['first-arg-is-ignored'], exit=False)<jupyter_output>...
----------------------------------------------------------------------
Ran 3 tests in 0.006s

OK<jupyter_text>In the code we firstly create a class which we define as “inheriting” from unittest's own inbuilt class TestCase. As before, this is a class that inherits the methods/properties of another class (see the previous session for an explanation of this). Under this we build three functions that will contain our tests, that will relate to the object (of the TestOrders class) we will be creating … fundamentally this is a testing object we are building, so we contain the "self" determination as we saw in the previous session. Each of our tests follows the same pattern:* We call the award_classifier function and pass the three expected variables (the marks for each component and the percentage of the final mark associated with modules). * We also state what result we would expect to see if the function works correctly. * For instance, the first test we have an average module mark of 65, a project mark of 75, and both these components have equal weighting (50% of total marks). For completeness we can do these maths:$ 65 * 0.5 = 32.5 $ \$ 75 * 0.5 = 37.5 $ \$ 32.5 + 37.5 = 70 $Accordingly, we would expect that algorithm should return "Distinction", and therefore we also provided this expected string. Finally, each test is wrapped in a unittest function assertEqual, which basically says that our test believes ("asserts") that the function return should equal "Distinction". The final part of our above test is a bit of code to automatically trigger the building of the class object and conducting of all tests whenever our code is called. The code starts with a strange looking if statement (if \_\_name\_\_ == '\_\_main\_\_') which you will see regularly used in Python programs. Its basically saying "if the code is run directly then do …", and that’s a pretty useful thing! In other words, we can run this code from anywhere that has a Python interpreter and it will execute the command that comes after the if. In this case the command translates as “run this file using the unittest module.However, we do have a little extra complication if we run the test via a Notebook (which we intend to here). Use the simpler code given in the comment if you are running the test on some other version of Python. Without going to much into this "hack", running without it would result in an error that causes the test to shutdown before it starts. If you really want to know more then here’s a Medium article: https://medium.com/@vladbezden/using-python-unittest-in-ipython-or-jupyter-732448724e31.<jupyter_code>if __name__ == '__main__':
    unittest.main(argv=['first-arg-is-ignored'], exit=False)<jupyter_output>...
----------------------------------------------------------------------
Ran 3 tests in 0.004s

OK<jupyter_text>As we expected, three tests have been conducted (pretty quickly on my machine too, so that’s nice). We also get the slightly anti-climatic print out “OK”. As this suggests, we passed all three tests (woo-hoo). If we wanted to see the opposite result, we can simply change the code so that we know a test would fail – e.g. changing the expected outcome of test_merit to “Pass”. If we run again we now get a less positive message:<jupyter_code>class TestOrders(unittest.TestCase):
    
    def test_distinction(self):
        self.assertEqual(award_classifier(65, 75, 0.5), 'Distinction')

    def test_fail(self):
        self.assertEqual(award_classifier(45, 54, 0.5), 'Fail')

    def test_merit(self):
        self.assertEqual(award_classifier(65, 55, 0.67), 'Pass') # changed to fail

if __name__ == '__main__':
    unittest.main(argv=['first-arg-is-ignored'], exit=False)<jupyter_output>..F
======================================================================
FAIL: test_merit (__main__.TestOrders)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "<ipython-input-5-d6a0c92427aa>", line 10, in test_merit
    self.assertEqual(award_classifier(65, 55, 0.67), 'Pass') # changed to fail
AssertionError: 'Merit' != 'Pass'
- Merit
+ Pass


----------------------------------------------------------------------
Ran 3 tests in 0.003s

FAILED (failures=1)
{'fraction_non_alphanumeric': 0.10793237971391417, 'fraction_numerical': 0.01536824683768767, 'mean_word_length': 3.6560264171711614, 'pattern_counts': {'":': 0, '<': 22, '<?xml version=': 0, '>': 19, 'https://': 4, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 1, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '5', 'dir': 'jupyter-structured-clean-dedup', 'id': '45923', 'license': 'MIT', 'n_tokens_mistral': 2302, 'n_tokens_neox': 2168, 'n_words': 1131, 'path': 'notebooks/4-03_unit_testing.ipynb', 'repo_name': 'Jordan-Bruno/example-repo', 'size': '8459'}