====== 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_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 ====
{{: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 'antidisestablishmentarianism']
# 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''.