Functional Tests

So far all the testing we’ve done has been unit testing, where we carefully isolate some piece of code and make assertions about how it (and only it) works.

It’s easy to get yourself into a situation where you mock yourself into placation, and the code doesn’t actually work anymore when you glue the pieces together even though all the unit tests pass. To avoid this situation, there are functional (or “system”) tests.

Adding a UI to Our Portfolio

Our portfolio code is, right now, essentially a library. It is a bit of code that is meant to be imported and used by other people. Let’s give it a UI and turn it in to something that normal people might see. In other words, let’s write an application that uses our Portfolio object.

Install Pyramid, pyramid_chameleon, waitress, and selenium:

$ pip install pyramid pyramid_chameleon waitress selenium

Add a views.py to our project (next to portfolio.py):

from pyramid.view import view_config

from .portfolio import Portfolio

@view_config(renderer='index.pt')
def index(request):
    p = Portfolio()
    p.buy('IBM', 100, 176.48)
    p.buy('HPQ', 100, 36.15)
    return {'portfolio':p}

Add an index.pt to our project (next to views.py):

<html>
<head></head>
<body>
<ul tal:define="current portfolio.current_prices()">
<li tal:repeat="stock portfolio.stocks">
  ${stock[0]} ${stock[1]} ${stock[2]} ${current[stock[0]]}
</li>
</ul>
</body>
</html>

Change our __init__.py so that it looks like this:

from waitress import serve
from pyramid.config import Configurator

def main():
    """Entry point for the application script"""
    config = Configurator()
    config.include('pyramid_chameleon')
    config.scan()
    app = config.make_wsgi_app()
    serve(app)

If we rerun setup.py develop, and run sample, we’ll see something like:

$ sample
serving on http://0.0.0.0:8080

We have now turned our library into an application. The application shows each stock’s name, the number of shares we own, its buying price, and its current value, one per line. We can make assertions about this output using selenium, a functional testing system.

Adding Functional Tests

Add a directory named functests to the sample project (next to tests). Add a file named __init__.py within it. Give it these contents:

# -*- coding: utf-8 -*-

import unittest

browser = None

# we create a global browser object for speed reasons.
# not ideal.. it breaks test isolation.

def setUpModule():
    from selenium.webdriver import Firefox
    global browser
    browser = Firefox()
    return browser

def tearDownModule():
    browser.quit()

class PageTests(unittest.TestCase):
    def test_render_default(self):
        browser.get('http://localhost:8080/')
        self.assertTrue('IBM 100 176.48' in browser.page_source)

Run nosetests sample.functests:

$ nosetests sample.functests
.
----------------------------------------------------------------------
Ran 1 test in 4.400s

OK

When you run these tests you will see the Firefox browser launched on your system briefly. Selenium is a system that executes browsers to ensure that web applications are operating properly.

We are using the nosetests script that is installed with nose for convenience. We could have changed things around such that setup.py nosetests executed the functional tests too, but we explicitly don’t want them to run when we run unit tests because when lots of test cases are added, they’re too slow, and people don’t run slow test suites.

Improving the Application

The template we wrote demonstrates that our Portfolio API is a little lame. Let’s improve it and evolve the template along with it, as well as adding a few more tests.

A Real World Test Suite

Let’s take a look at the Deformdemo application to get a sense of a real-world test suite for an application. https://github.com/Pylons/deformdemo

Follow the instructions in its README.rst to be able to run its test suite. Do this is a new shell, unrelated to the portfolio demo.

(Where it says virtualenv2.7 instead use virtualenv -ppy27).

Take a look at the tests defined in deformdemo/test.py (https://github.com/Pylons/deformdemo/blob/master/deformdemo/test.py).

Note that you can:

  • Click buttons.
  • Fill in form fields.
  • Make assertions about the contents of HTML elements.
  • Find specific elements using XPath, CSS, or by unique HTML id.
  • Send keystrokes.
  • Test the result of executing JavaScript (such as drag and drop).

Selenium can be driven using Python, and I tend to use it that way. It can also be driven via a UI (Selenium-UI), or by bindings for most other dynamic languages.