Custom Exceptions in python

While writing tests we often need to compare objects defined by us and we then have to compare each attribute of the object one by one.

Let's take an example of a simple Car class. The Car object has two attributes speed and color.

class Car:
    def __init__(self, speed, color):
        self.speed = speed
        self.color = color

So if you try writing test for it using unittest.TestCase.AssertEqual method.

import unittest

class TestCar(unittest.TestCase):
    def test_car_equal(self):
       car1 = Car(40, 'red')
       car2 = Car(40, 'red')
       self.assertEqual(car1, car2) 

If you now try to run this test, the test case will fail.

    self.assertEqual(Car(40, 'red'), Car(40, 'red'))
AssertionError: <assert.Car object at 0x7f8edbc078d0> != <assert.Car object at 0x7f8edbc07908>

This is because unittest doesn't know how to compare two Car objects.

So, we have a two options to make this work:
1. We can define a __eq__ method in Car class.
2. We can create a custom assertion class.

1. __eq__ method

__eq__ is one of the rich comparison methods. Whenever == is used on any object its __eq__ method is called. For example car1 == car2 internally calls car1.__eq__(car2).

Our new car class:

class Car:
   def __init__(self, speed, color):
      self.speed = speed
      self.color = color

   def __eq__(self, another_car):
       if type(self)==type(another_car) and self.speed==another_car.speed and
               self.color==another_car.color:
           return True
        else:
           return False

And now the same test passes. But when car1 is not equal to car2, the test fails and the output is not at all informative about why the test failed. It simply prints:

    self.assertEqual(car1, car2)
AssertionError: <Car object at 0x7f1f86af6a20> != <Car object at 0x7f1f86af6a58>

So, one drawback of this approach is that we can't show some message about why the equality failed. Showing an error message by printing it wouldn't be a good design because when the user does a simple car1 == car2 and if they are not equal, __eq__ would still print the details of why they are not equal.

Now, we see the second approach in which we can give a detailed message about why the equality failed.

2. Custom assertion class

We can write a custom assertion class for out car class.

class AssertCarEqual:
    def assert_car_equal(self, car1, car2):
        if type(car1) != type(car2):
            raise AssertionError("car1 is of type: ", type(car1), " and car2 is of type: ", type(car2))
        elif car1.speed != car2.speed:
            raise AssertionError("speed of car1: ", car1.speed, " and speed of car2: ", car2.speed)
        elif car1.color != car2.color:
            raise AssertionError("color of car1: ", car1.color, " and color of car2: ", car2.color)

And modifying the test like this:

class TestCar(unittest.TestCase, AssertCarEqual):
    def test_car_equal(self):
        car1 = car.Car(40, 'blue')
        car2 = car.Car(40, 'red')
        self.assert_car_equal(car1,car2)

Now when we run the test, the test fails and gives a message about why the test failed.

    raise AssertionError("color of car1: ", car1.color, " and color of car2: ", car2.color)
AssertionError: ('color of car1: ', 'blue', ' and color of car2: ', 'red')