====== Computer Lab 13, unit tests, exceptions, list comprehensions ====== ===== Class inheritance ===== **BASE CLASS** class Person: def __init__(self, fname, lname): self.firstname = fname self.lastname = lname def printname(self): print(self.firstname, self.lastname) if __name__ == "__main__": p1 = Person("Jane", "Doe") # p2 = Person("Jane", "Doe", 2019) won't work p1.printname() # p1.welcome() won't work **CHILD CLASS** class Student(Person): def __init__(self, fname, lname, year): super().__init__(fname, lname) self.graduationyear = year def welcome(self): print("Welcome", self.firstname, self.lastname, "to the class of", self.graduationyear) if __name__ == "__main__": s1 = Student("Mike", "Olsen", 2019) s1.printname() s1.welcome() ===== Unit tests ===== * Execution of your test plan by a script instead of a human. * A unit test is a smaller test, one that checks that a single component operates in the right way. A unit test helps you to isolate what is broken in your application and fix it faster. * Note: If you find that the unit of code you want to test has lots of side effects, you might be breaking the [[https://en.wikipedia.org/wiki/Single-responsibility_principle|Single Responsibility Principle]]. Breaking the Single Responsibility Principle means the piece of code is doing too many things and would be better off being refactored. Following the Single Responsibility Principle is a great way to design code that is easy to write repeatable and simple unit tests for, and ultimately, reliable applications. * Then the structure of a test should follow this workflow: - Create your inputs - Execute the code being tested, capturing the output - Compare the output with an expected result. Validation of the output against a known response is known as an **assertion**. * There are many test runners available for Python. The one built into the Python standard library is called //unittest//. * //unittest// contains both a testing framework and a test runner. * //unittest// has some important requirements for writing and executing tests: - You put your tests into classes as methods - You use a series of special assertion methods in the unittest.TestCase class instead of the built-in assert statement. - Here are some of the most commonly used methods: ^ Method ^ Equivalent to | | ''.assertEqual(a, b)'' | ''a == b'' | | ''.assertNotEqual(a, b)'' | ''a != b'' | | ''.assertAlmostEqual(a, b)'' | ''round(a-b, 7) == 0'' | | ''.assertGreater(a, b)'' | ''a > b'' | | ''.assertTrue(x)'' | ''bool(x) is True'' | | ''.assertFalse(x)'' | ''bool(x) is False'' | | ''.assertIs(a, b)'' | ''a is b'' | | ''.assertIsNone(x)'' | ''x is None'' | | ''.assertIn(a, b)'' | ''a in b'' | | ''.assertIsInstance(a, b)'' | ''isinstance(a, b)'' | | ''.assertRaises(SomeException)'' | | ==== Testing modules/libraries ==== File structure: /prg_12 ...__init__.py (empty file) ...mymath.py .../tests ......__init__.py (empty file) ......test_mymath.py //mymath.py// (module to test) def abs(x): if isinstance(x, int) or isinstance(x, float): if x >= 0: return x else: return -x else: raise ValueError def aprox_cos(x): if isinstance(x, int) or isinstance(x, float): return 1 - x ** 2 / 2 + x ** 4 / 24 else: raise ValueError def aprox_sin(x): if isinstance(x, int) or isinstance(x, float): return x - x ** 3 / 6 + x ** 5 / 120 else: raise ValueError //test_mymath.py// (unit tests) import unittest from prg_13_test.mymath import abs class TestMyMath(unittest.TestCase): def test_abs_positive(self): """ Test that abs() of a positive number is exactly the same number """ data = 8 result = abs(data) self.assertEqual(result, 8) def test_abs_negative(self): """ Test that abs() of a negative number is an additive inverse of the same number """ data = -8 result = abs(data) self.assertEqual(result, 8) def test_abs_string(self): """ Test that abs() raises a ValueError when it is called with a string. """ data = "666" with self.assertRaises(ValueError): result = abs(data) ==== Testing classes ==== //point.py// (class definition) import math class Point: def __init__(self, x, y): self.x = x self.y = y def __str__(self): return "x = {}, y = {}".format(self.x, self.y) def __add__(self, other): return Point(self.x + other.x, self.y + other.y) def get_norm(self): norm = math.sqrt(self.x ** 2 + self.y ** 2) return norm def is_unit(self): return self.get_norm() == 1 def __eq__(self, other): return self.x == other.x and self.y == other.y //test_point.py// (unit test) import unittest from prg_13_test.point import Point class TestPoint(unittest.TestCase): def setUp(self): self.p = Point(2, 5) def test_init(self): ''' Test that p.x and p.y are set correctly. ''' self.assertEqual(self.p.x, 2) self.assertEqual(self.p.y, 5) def test_str(self): ''' Test that the __str__() returns a string in a correct format. ''' string_p = str(self.p) self.assertEqual(string_p, "x = 2, y = 5") def test_eq(self): ''' Test that the __eq__() function works as expected. ''' self.assertEqual(self.p, Point(2, 5)) self.assertNotEqual(self.p, Point(0, 5)) self.assertNotEqual(self.p, Point(2, 0)) self.assertNotEqual(self.p, Point(0, 0)) def test_add(self): ''' Test that the + operation return a correct type and answer ''' p2 = Point(-5, 8) sum_p = self.p + p2 self.assertIsInstance(sum_p, Point) self.assertEqual(sum_p, Point(-3, 13)) def test_is_unit(self): ''' Test that the test_is_unit() works as expected. ''' p1 = Point(1, 0) p2 = Point(0, 1) p3 = Point(1, 1) p4 = Point(3/5, 4/5) self.assertTrue(p1.is_unit()) self.assertTrue(p2.is_unit()) self.assertFalse(p3.is_unit()) self.assertTrue(p4.is_unit) ==== Custom exceptions ==== {{:courses:be5b33prg:labs:exception_hierarchy_crop.jpg?direct| }} * ''Exception'' is the most commonly-inherited exception type (outside of the true base class of BaseException). * In addition, all exception classes that are considered errors are subclasses of the ''Exception'' class. * In general, any custom exception class you create in your own code should inherit from ''Exception''. * The ''Exception'' class contains many direct child subclasses that handle most Python errors: ArithmeticError, AssertionError, ImportError, MemoryError, NameError, StopIteration, TypeError, ValueError, etc. * When implementing your custom exception, you need to implement ''%%__init__()%%'' and ''%%__str__()%%'' fuctions. * Raising exception means actually creating an exception instance (object) and printing it at the same time. In previous weeks, you were asked to implement the following piecewise function: \[ f(x)= \begin{cases} (x-6)^2+5 & 6 \le x \\ 5 & 4 \leq x\lt 6 \\ \frac{5}{2}x-5 & 2 \leq x\lt 4 \\ \text{undefined} & x \lt 2 \end{cases} \] * Let's create our own exception ''UndefinedFunctionValueError'' which will be raised for such //x// when //f(x)// is undefined. //Init() and str() implemented.// class UndefinedFunctionValueError(Exception): def __init__(self, message): self.message = message def __str__(self): return 'UndefinedFunctionValueError: {}'.format(self.message) def piecewise_function(x): if x >= 6: return (x - 6) ** 2 + 5 elif x >= 4: return 5 elif x >= 2: return 5 / 2 * x - 5 else: raise UndefinedFunctionValueError("The Function is not defined for: {}.".format(x)) if __name__ == "__main__": x1 = piecewise_function(8) print("f(8) = ", x1) x2 = piecewise_function(0) print("f(0) = ", x2) //Exception takes a number instead of a string.// class UndefinedFunctionValueError(Exception): def __init__(self, x_value): self.x_value = x_value def __str__(self): return 'UndefinedFunctionValueError: The Function is not defined for: {}'.format(self.x_value) def piecewise_function(x): if x >= 6: return (x - 6) ** 2 + 5 elif x >= 4: return 5 elif x >= 2: return 5 / 2 * x - 5 else: raise UndefinedFunctionValueError(x) if __name__ == "__main__": x1 = piecewise_function(8) print("f(8) = ", x1) x2 = piecewise_function(0) print("f(0) = ", x2) //A class uses its parent class.// class UndefinedFunctionValueError(Exception): def __init__(self, x_value): message = "The Function is not defined for: {}.".format(x_value) super().__init__(message) def piecewise_function(x): if x >= 6: return (x - 6) ** 2 + 5 elif x >= 4: return 5 elif x >= 2: return 5 / 2 * x - 5 else: raise UndefinedFunctionValueError(x) if __name__ == "__main__": x1 = piecewise_function(8) print("f(8) = ", x1) x2 = piecewise_function(0) print("f(0) = ", x2) //Inheritance from the ValueError class.// class UndefinedFunctionValueError(ValueError): def __init__(self, message): self.message = message def __str__(self): return 'UndefinedFunctionValueError: {}'.format(self.message) def piecewise_function(x): if x >= 6: return (x - 6) ** 2 + 5 elif x >= 4: return 5 elif x >= 2: return 5 / 2 * x - 5 else: raise UndefinedFunctionValueError("The Function is not defined for: {}.".format(x)) if __name__ == "__main__": try: x1 = piecewise_function(8) print("f(8) = ", x1) except UndefinedFunctionValueError as e: print(e) try: x2 = piecewise_function(0) print("f(0) = ", x2) except UndefinedFunctionValueError as e: print(e) ===== List Comprehension ===== * Shorter syntax when you want to create a new list based on the values of an existing list (iterable, in general). * Syntax: newlist = [expression for item in iterable if condition == True] * The //condition// is like a filter that accepts only the items that valuate to ''True'': fruits = ["apple", "banana", "cherry", "kiwi", "mango"] newlist = [x for x in fruits if x != "apple"] sentence = 'the rocket came back from mars' >>> vowels = [i for i in sentence if i in 'aeiou'] >>> vowels ['e', 'o', 'e', 'a', 'e', 'a', 'o', 'a'] * The //condition// is optional and can be omitted: newlist = [x for x in fruits] * The //iterable// can be any iterable object, e.g., a list, tuple, set, string etc: # list fruits = ["apple", "banana", "cherry", "kiwi", "mango"] newlist1 = [x for x in fruits] newlist2 = [x for x in fruits if 'a' in x] newlist3 = [x for x in fruits if x != "apple"] # string anti_letters = [letter for letter in 'anti­dis­establishment­arian­ism'] # range range_list1 = [x for x in range(10)] range_list2 = [x for x in range(10) if x < 5] The //expression// is the current item in the iteration, but it is also the outcome, which you can manipulate before it ends up like a list item in the new list: squares = [i * i for i in range(10)] fruits = ["apple", "banana", "cherry", "kiwi", "mango"] newlist = [x.upper() for x in fruits] **Weekly homework 13** - Implement a custom exception with a class ''InvalidPasswordFormatError'' (inherits from ''ValueError''). - Its ''%%__str__()%%'' method should return a string in the following format: ''Reason: ''. - Implement a fuction (in the global scope) ''is_valid_password(pswd)'' which works as suggested by following listings: if __name__ == "__main__": try: print(is_valid_password("abcdefghij")) except InvalidPasswordFormatError as e: print(e) True if __name__ == "__main__": try: print(is_valid_password("abcde")) except InvalidPasswordFormatError as e: print(e) Reason: Your password is too short. if __name__ == "__main__": try: print(is_valid_password("abcdefefefe8")) except InvalidPasswordFormatError as e: print(e) Reason: Your password must contain only letters. if __name__ == "__main__": try: print(is_valid_password("abcd8e")) except InvalidPasswordFormatError as e: print(e) Reason: Your password is too short. Your password must contain only letters. * Password is too short if it is shorter than 8 characters. * Place both the class definition (the custom exception) and the function into a single module ''13_weekly_hw.py''.