Warning
This page is located in archive.

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 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:
  1. Create your inputs
  2. Execute the code being tested, capturing the output
  3. 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:
  1. You put your tests into classes as methods
  2. You use a series of special assertion methods in the unittest.TestCase class instead of the built-in assert statement.
  3. 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_13
...mymath.py
.../tests
......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 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 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

  • 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

  1. Implement a custom exception with a class InvalidPasswordFormatError (inherits from ValueError).
  2. Its __str__() method should return a string in the following format: Reason: <string passed to the constructor of the object>.
  3. 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.
courses/be5b33prg/labs/week_13.txt · Last modified: 2021/12/16 10:43 by nemymila