Testing

Tests are central to our development process. We depend on a testing infrastructure build using Fixtures and some other testing tools.

Fixtures

A selection of Fixtures are described in the reference docs. Here is a short summary of what each is used for:

ReahlSystemFixture and ReahlSystemSessionFixture
These are used to set up a database connection and create its schema once, at the start of a test run. They give access to the global system Configuration, the ExecutionContext in which tests using them are run, and to a SystemControl — an object used to control anything related to the database.
SqlAlchemyFixture
If a test uses a SqlAlchemyFixture, a database transaction is started before the test is run, and rolled back after it completed. This ensures that the database state stays clean between test runs. SqlAlchemyFixture can also create tables (and associated persisted classes) on the fly for use in tests.
WebFixture and WebServerFixture
These Fixtures work together to start up a web server, and a selenium-driven web browser for use in tests. The web server runs in the same thread and database transaction as your tests. This means that if a breakage occurs server-side your test will break immediately, with a relevant stack trace. It also means that the idea of rolling back transactions after each test still works, even when used with Selenium tests.

Web browser interfaces

Other important tools introduced are: Browser and XPath. Browser is not a real browser – it is our thin wrapper on top of what is available from WebTest. Browser has the interface of a Browser though. It has methods like .open() (to open an url), .click() (to click on anything), and .type() (to type something into an element of the web page).

We like this interface, because it makes the tests more readable.

When the browser is instructed to click on something or type into something, an XPath expression is used to specify how to find that element on the web page. XPath has handy methods for constructing XPath expressions while keeping the code readable. Compare, for example the following two lines to appreciate what XPath does. Isn’t the one using XPath much more explicit and readable?

browser.click('//a[href="/news"]')
browser.click(XPath.link_labelled('News'))

Readable tests

Often in tests, there are small bits of code which would be more readable if it was extracted into properly named methods (as done with XPath above). If you create a Fixture specially for testing the AddressBookUI (in the example below), such a fixture is an ideal home for extracted methods. In the test below, AddressAppFixture contains the nasty implementation of a couple of things we’d like to assert about the application, as well as some objects used by the tests.

from __future__ import print_function, unicode_literals, absolute_import, division

from reahl.tofu import Fixture, uses
from reahl.tofu.pytestsupport import with_fixtures
from reahl.webdev.tools import Browser, XPath
from reahl.doc.examples.tutorial.parameterised2.parameterised2 import AddressBookUI, Address
from reahl.web_dev.fixtures import WebFixture


@uses(web_fixture=WebFixture)
class AddressAppFixture(Fixture):

    def new_browser(self):
        return Browser(self.web_fixture.new_wsgi_app(site_root=AddressBookUI))

    def new_existing_address(self):
        address = Address(name='John Doe', email_address='[email protected]')
        address.save()
        return address

    def is_on_home_page(self):
        return self.browser.title == 'Show'
        
    def is_on_add_page(self):
        return self.browser.title == 'Add'

    def is_on_edit_page_for(self, address):
        return self.browser.title == 'Edit %s' % address.name

    def address_is_listed_as(self, name, email_address):
        return self.browser.is_element_present(XPath.paragraph_containing('%s: %s' % (name, email_address)))


@with_fixtures(WebFixture, AddressAppFixture)
def test_adding_an_address(web_fixture, address_app_fixture):
    """To add a new address, a user clicks on "Add Address" link on the menu, then supplies the 
       information for the new address and clicks the Save button. Upon success addition of the
       address, the user is returned to the home page where the new address is now listed."""

    browser = address_app_fixture.browser

    browser.open('/')
    browser.click(XPath.link_with_text('Add'))

    assert address_app_fixture.is_on_add_page()
    browser.type(XPath.input_labelled('Name'), 'John Doe')
    browser.type(XPath.input_labelled('Email'), '[email protected]')
    browser.click(XPath.button_labelled('Save'))

    assert address_app_fixture.is_on_home_page()
    assert address_app_fixture.address_is_listed_as('John Doe', '[email protected]') 


@with_fixtures(WebFixture, AddressAppFixture)
def test_editing_an_address(web_fixture, address_app_fixture):
    """To edit an existing address, a user clicks on the "Edit" button next to the chosen Address
       on the "Addresses" page. The user is then taken to an "Edit" View for the chosen Address and 
       can change the name or email address. Upon clicking the "Update" Button, the user is sent back 
       to the "Addresses" page where the changes are visible."""

    browser = address_app_fixture.browser
    existing_address = address_app_fixture.existing_address

    browser.open('/')
    browser.click(XPath.button_labelled('Edit'))

    assert address_app_fixture.is_on_edit_page_for(existing_address)
    browser.type(XPath.input_labelled('Name'), 'John Doe-changed')
    browser.type(XPath.input_labelled('Email'), '[email protected]')
    browser.click(XPath.button_labelled('Update'))

    assert address_app_fixture.is_on_home_page()
    assert address_app_fixture.address_is_listed_as('John Doe-changed', '[email protected]') 
                

Testing JavaScript

The Browser class used above cannot be used for all tests, since it cannot execute javascript. If you want to test something which makes use of javascript, you’d need the tests (or part of them) to execute in something like an actual browser. Doing this requires Selenium, and the use of the web server started by WebFixture for exactly this eventuality.

DriverBrowser is a class which provides a thin layer over Selenium’s WebDriver interface. It gives a programmer a similar set of methods to those provided by the Browser, as used above. An instance of it is available on the WebFixture via its .driver_browser attribute.

The standard Fixtures that form part of Reahl use Chromium by default in order to run tests, and they expect the executable for chromium to be in ‘/usr/lib/chromium-browser/chromium-browser’.

You can change which browser is used by creating a new Fixture that inherits from WebFixture, and overriding its .new_driver_browser() method.

When the following is executed, a browser will be fired up, and Selenium used to test that the validation provided by EmailField works in javascript as expected. Note also how javascript is enabled for the web application upon creation:

from __future__ import print_function, unicode_literals, absolute_import, division

from reahl.tofu import Fixture, uses
from reahl.tofu.pytestsupport import with_fixtures
from reahl.webdev.tools import XPath
from reahl.doc.examples.tutorial.parameterised2.parameterised2 import AddressBookUI, Address
from reahl.web_dev.fixtures import WebFixture


@uses(web_fixture=WebFixture)
class AddressAppFixture(Fixture):

    def new_wsgi_app(self):
        return self.web_fixture.new_wsgi_app(site_root=AddressBookUI, enable_js=True)

    def new_existing_address(self):
        address = Address(name='John Doe', email_address='[email protected]')
        address.save()
        return address

    def error_is_displayed(self, text):
        return self.web_fixture.driver_browser.is_element_present(XPath.span_containing(text))


@with_fixtures(WebFixture, AddressAppFixture)
def test_edit_errors(web_fixture, address_app_fixture):
    """Email addresses on the Edit an address page have to be valid email addresses."""

    web_fixture.reahl_server.set_app(address_app_fixture.wsgi_app)
    browser = web_fixture.driver_browser
    address_app_fixture.new_existing_address()

    browser.open('/')
    browser.click(XPath.button_labelled('Edit'))

    browser.type(XPath.input_labelled('Email'), 'invalid email address')
    browser.press_tab()  #tab out so that validation is triggered

    assert address_app_fixture.error_is_displayed('Email should be a valid email address')