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

Using TDD to build a stock alert application


Over the course of this book, we are going to be using TDD to build a simple stock alert application. The application will listen to stock updates from a source. The source can be anything—a server on the Internet, or a file on the hard drive, or something else. We will be able to define rules, and when the rule is matched, the application sends us an email or text message.

For example, we could define a rule as "If AAPL crosses the $550 level then send me an email". Once defined, the application will monitor updates and send an e-mail when the rule is matched.

Writing our first test

Enough talk. Let's get started with our application. What is a good place to start? From examining the application description mentioned earlier, it looks like we will need the following modules:

  • Some way to read stock price updates, either from the Internet or from a file

  • A way to manage the stock information so that we can process it

  • A way to define rules and match them against the current stock information

  • A way to send an email or text message when a rule is matched

Based on these requirements, we will be using the following design:

Each term is discussed as follows:

  • Alert: This is the core of the application. An alert will take a Rule and map it to an Action. When the rule is matched, the action is executed.

  • Rule: A Rule contains the condition we want to check for. We should get alerted when the rule is matched.

  • Action: This is the action to be performed when the rule is matched. This could be as simple as printing a message on the screen, or, in more real-work scenarios, we might send an e-mail or a text message.

  • Stock: The Stock class keeps track of the current price and possibly a history of the prices for a stock. It sends an Event to the Alert when there is an update. The alert then checks if it's rule matched and whether any action needs to be executed.

  • Event: This class is used to send events to the Alert when a Stock is updated.

  • Processor: The processor takes stock updates from the Reader and updates the Stock with the latest data. Updating the stock causes the event to be fired, which, in turn, causes the alert to check for a rule match.

  • Reader: The Reader gets the stock alerts from some source. In this book, we are going to get updates from a simple list or a file, but you can build other readers to get updates from the Internet or elsewhere.

Among all these classes, the way to manage stock information seems to be the simplest, so let's start there. What we are going to do is to create a Stock class. This class will hold information about the current stock. It will store the current price and possibly some recent price history. We can then use this class when we want to match rules later on.

To get started, create a directory called src. This directory is going to hold all our source code. In the rest of this book, we will refer to this directory as the project root. Inside the src directory, create a subdirectory called stock_alerter. This is the directory in which we are going to implement our stock alert module.

Okay, let's get started with implementing the class.

NO! Wait! Remember the TDD process that was described earlier? The first step is to write a test, before we code the implementation. By writing the test first, we now have the opportunity to think about what we want this class to do.

So what exactly do we want this class to do? Let's start with something simple:

  • A Stock class should be instantiated with the ticker symbol

  • Once instantiated, and before any updates, the price should be None

Of course, there are many more things we will want this class to do, but we'll think about them later. Rather than coming up with a very comprehensive list of functionality, we're going to focus on tiny bits of functionality, one at a time. For now, the preceding expectation is good enough.

To convert the preceding expectation into code, create a file called stock.py in the project root, and put the following code in it:

import unittest
class StockTest(unittest.TestCase):
    def test_price_of_a_new_stock_class_should_be_None(self):
        stock = Stock("GOOG")
        self.assertIsNone(stock.price)
if __name__ == "__main__":
    unittest.main()

What does this code do?

  1. First, we import unittest. This is the library that has the test framework that we are going to use. Luckily for us, it is bundled into the Python standard library by default and is always available, so we don't need to install anything, we can just import the module directly.

  2. Second, we create a class StockTest. This class will hold all the test cases for the Stock class. This is just a convenient way of grouping related tests together. There is no rule that every class should have a corresponding test class. Sometimes, if we have a lot of tests for a class, then we may want to create separate test classes for each individual behavior, or group the tests some other way. However, in most cases, creating one test class for an actual class is the best way to go about it.

  3. Our StockTest class inherits from the TestCase class in the unittest module. All tests need to inherit from this class in order to be identified as a test class.

  4. Inside the class, we have one method. This method is a test case. The unittest framework will pick up any method that starts with test. The method has a name that describes what the test is checking for. This is just so that when we come back after a few months, we still remember what the test does.

  5. The test creates a Stock object and then checks if the price is None. assertIsNone is a method provided by the TestCase class that we are inheriting from. It checks that its parameter is None. If the parameter is not None, it raises an AssertionError and fails the test. Otherwise, execution continues to the next line. Since that is the last line of the method, the test completes and is marked as a pass.

  6. The last segment checks if the module was executed directly from the command line. In such a case, the __name__ variable will have the value __main__, and the code will execute the unittest.main() function. This function will scan the current file for all tests and execute them. The reason we need to wrap this function call inside the conditional is because this part does not get executed if the module is imported into another file.

Congratulations! You have your first failing test. Normally, a failing test would be a cause for worry, but in this case, a failing test means that we're done with the first step of the process and can move on to the next step.

Analyzing the test output

Now that we've written our test, it is time to run it. To run the test, just execute the file. Assuming that the current directory is the src directory, the following is the command to execute the file:

  • Windows:

    python.exe stock_alerter\stock.py
    
  • Linux/Mac:

    python3 stock_alerter/stock.py
    

If the python executable is not on your path, then you will have to give the full path to the executable here. In some Linux distributions, the file may be called python34 or python3.4 instead of python3.

When we run the file, the output looks like the following:

E
=====================================================================
ERROR: test_price_of_a_new_stock_class_should_be_None (__main__.StockTest)
---------------------------------------------------------------------
Traceback (most recent call last):
  File "stock_alerter\stock.py", line 6, in test_price_of_a_new_stock_class_should_be_None
    stock = Stock("GOOG")
NameError: name 'Stock' is not defined
---------------------------------------------------------------------
Ran 1 test in 0.001s

FAILED (errors=1)

As expected, the test fails, because we haven't created the Stock class yet.

Let's look at that output in a little more detail:

  • E on the first line signifies that the test gave an error. If a test passed, then you would have a dot on that line. A failed test would be marked with F. Since we have only a single test, there is only one character there. When we have multiple tests, then the status of each test will be displayed on that line, one character per test.

  • After all the test statuses are displayed, we get a more detailed explanation of any test errors and failures. It tells us whether there was a failure or an error (in this case denoted by ERROR) followed by the name of the test and which class it belongs to. This is followed by a traceback, so we know where the failure occurred.

  • Finally, there is a summary that shows how many tests were executed, how many passed or failed, and how many gave errors.

Test errors versus test failures

There are two reasons why a test might not pass: It might have failed or it might have caused an error. There is a small difference between these two. A failure indicates that we expected some outcome (usually via an assert), but got something else. For example, in our test, we are asserting that stock.price is None. Suppose stock.price has some other value apart from None, then the test will fail.

An error indicates that something unexpected happened, usually an unexpected exception was raised. In our previous example, we got an error because the Stock class has not yet been defined.

In both the cases, the test does not pass, but for different reasons, and these are reported separately as test failures and test errors.

Making the test pass

Now that we have a failing test, let's make it pass. Add the following code to the stock.py file, after the import unittest line:

class Stock:
    def __init__(self, symbol):
        self.symbol = symbol
        self.price = None

What we have done here is to implement just enough code to pass the test. We've created the Stock class so the test shouldn't complain about it being missing, and we've initialized the price attribute to None.

What about the rest of the implementation for this class? This can wait. Our main focus right now is to pass the current expectation for this class. As we write more tests, we will end up implementing more of the class as well.

Run the file again, and this time the output should be like the following:

.
---------------------------------------------------------------------
Ran 1 test in 0.000s

OK

We've got a dot in the first line, which signifies that the test is passing. The OK message at the end tells us that all tests have passed.

The final step is to refactor the code. With so little code, there is really nothing much to clean up. So, we can skip the refactoring step and start with the next test.