Book Image

Test-Driven Python Development

By : Siddharta Govindaraj
Book Image

Test-Driven Python Development

By: Siddharta Govindaraj

Overview of this book

This book starts with a look at the test-driven development process, and how it is different from the traditional way of writing code. All the concepts are presented in the context of a real application that is developed in a step-by-step manner over the course of the book. While exploring the common types of smelly code, we will go back into our example project and clean up the smells that we find. Additionally, we will use mocking to implement the parts of our example project that depend on other systems. Towards the end of the book, we'll take a look at the most common patterns and anti-patterns associated with test-driven development, including integration of test results into the development process.
Table of Contents (20 chapters)
Test-Driven Python Development
Credits
About the Author
Acknowledgments
About the Reviewers
www.PacktPub.com
Preface
Index

Red-Green-Refactor – The TDD Cycle


This exercise asks us to add support for updates which come out of order, that is, a newer update is followed by an older update. We need to use the timestamp to determine which update is newer and which is older.

The following is a test case for this requirement:

def test_price_is_the_latest_even_if_updates_are_made_out_of_order(self):
    self.goog.update(datetime(2014, 2, 13), price=8)
    self.goog.update(datetime(2014, 2, 12), price=10)
    self.assertEqual(8, self.goog.price)

In the test above, we first give the update for February 13, followed by the update for February 12. We then assert that the price attribute returns the latest price (for February 13). The test fails of course.

In order to make this test pass, we can't simply add the latest update to the end of the price_history list. We need to check the timestamp and insert it accordingly into the list, keeping it sorted by timestamp.

The bisect module provided in the Python standard library contains the insort_left function that inserts into a sorted list. We can use this function as follows (remember to import bisect at the top of the file):

def update(self, timestamp, price):
    if price < 0:
        raise ValueError("price should not be negative")
    bisect.insort_left(self.price_history, (timestamp, price))

In order to have a sorted list, the price_history list needs to keep a list of tuples, with the timestamp as the first element. This will keep the list sorted by the timestamp. When we make this change, it breaks our other methods that expect the list to contain the price alone. We need to modify them as follows:

@property
def price(self):
    return self.price_history[-1][1] \
        if self.price_history else None

def is_increasing_trend(self):
    return self.price_history[-3][1] < \
        self.price_history[-2][1] < self.price_history[-1][1]

With the above changes, all our existing tests as well as the new test start passing.

Now that we have the tests passing, we can look at refactoring the code to make it easier to read. Since the price_history list now contains tuples, we have to refer to the price element by tuple index, leading to statements list price_history[-1][1], which are not very clear. We can make this clearer by using a named tuple that allows us to assign names to the tuple values. Our refactored Stock class now looks like the following:

PriceEvent = collections.namedtuple("PriceEvent", ["timestamp", "price"])

class Stock:
    def __init__(self, symbol):
        self.symbol = symbol
        self.price_history = []

    @property
    def price(self):
        return self.price_history[-1].price \
            if self.price_history else None

    def update(self, timestamp, price):
        if price < 0:
            raise ValueError("price should not be negative")
        bisect.insort_left(self.price_history, PriceEvent(timestamp, price))

    def is_increasing_trend(self):
        return self.price_history[-3].price < \
            self.price_history[-2].price < \
                self.price_history[-1].price

After the change, we run the tests to ensure that everything still works.