Testing¶
Tests are central to our development process. We depend on a testing
infrastructure build using Fixture
s and some other testing tools.
Fixtures¶
A selection of Fixture
s are described in the reference docs. Here is a short summary of what each is used for:
ReahlSystemFixture
andReahlSystemSessionFixture
- 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
, theExecutionContext
in which tests using them are run, and to aSystemControl
— 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
andWebServerFixture
- 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 Fixture
s 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')