Testing programs with objects; inheritance

Testing programs with objects

How should we test our library program? We should write tests for each of the methods. Let’s test the Book class:

from library import *

def test_init():
    b = Book("The Nickel Boys", "Colson Whitehead")
    assert b.title == "The Nickel Boys"
    assert b.author == "Colson Whitehead"

def test_matches():
    b = Book("The Nickel Boys", "Colson Whitehead")
    assert b.matches("Nickel")
    assert not b.matches("Parasite")
    assert b.matches("Colson")

Notice the pattern here: we’re creating objects in our test methods in order to test their methods.

Adding checkout

Let’s say we want to add “checkout” behavior to our library: we want to be able to record that certain items have been checked out or returned, and take this into account when searching. Let’s add this behavior to our tests first:

def test_init():
    b = Book("The Nickel Boys", "Colson Whitehead")
    assert b.title == "The Nickel Boys"
    assert b.author == "Colson Whitehead"
    assert not b.checked_out

def test_checkout():
    b = Book("The Nickel Boys", "Colson Whitehead")
    b.checkout()
    assert b.checked_out

def test_return():
    b = Book("The Nickel Boys", "Colson Whitehead")
    b.checkout()
    b.return()
    assert not b.checked_out


def test_matches():
    b = Book("The Nickel Boys", "Colson Whitehead")
    assert b.matches("Nickel")
    assert not b.matches("Parasite")
    assert b.matches("Colson")
    b.checkout()
    assert not b.matches("Nickel")
    assert not b.matches("Parasite")
    assert not b.matches("Colson")

Then we can add the behavior to our Book class:

class Book:
    def __init__(self, title: str, author: str):
        self.title = title
        self.author = author
        self.checked_out = False

    def checkout(self):
        self.checked_out = True

    def return(self):
        self.checked_out = False

   def matches(self, query: str) -> bool:
       return (not self.checked_out) and (query in self.title or query in self.author)

Inheritance

Right now, we’re considering each class to be totally separate, with no shared code or data. For instance, even though Book~s and ~Movie~s both implement a ~matches method, the two methods do are have completely different implementations. Inheritance gives us a way to share code between objects. Classes can inherit from other classes, like this:

class A:
    pass

class B(A):
    pass

When there’s an inheritance relationship like this between two classes, we sat that A is the superclass of B and B is a subclass of A.

Let’s see an example of inheritance in action. Let’s say our library wants to track which items have been checked out, and only return items in a search if they are actually available. First, we’ll implement a LibraryItem class, which we’ll use as the superclass for both books and movies:

class LibraryItem:
    def __init__(self):
        self.checked_out = False

    def checkout(self):
        self.checked_out = True

    def return(self):
        self.checked_out = False

The LibraryItem class only handles checking items out and back in–it doesn’t know anything about what the items actually are.

In order to use our new superclass, we’ll make some changes to the Book and Movie classes:

class Book(LibraryItem):
    def __init__(self, title: str, author: str):
        self.title = title
        self.author = author
        super().__init__(self)

class Movie(LibraryItem):
    def __init__(self, title: str, director: str, actors: list):
        self.title = title
        self.director = director
        self.actors = actors
        super().__init__(self)

Both Book and Movie objects will now have a checked_out field, as well as the checkout and return methods. We can use the field in each class’s matches method:

class Book(LibraryItem):
      # other methods elided...
      def matches(self, query: str) -> bool:
          return (not self.checked_out) and (query in self.title or query in self.author)

class Movie(LibraryItem):
      # other methods elided...
      def matches(self, query: str) -> bool:
          return (not self.checked_out) and
             query in self.title or query in self.director or 
             any([query in actor for actor in self.actors])