I first adapted the script testRunner.py to include UI testing using the Selenium webdriver module and made some more changes.
When testing you want to use a separate database to prevent polluting the production database. The original suggestion was to add a few lines to the model file.
# create a test database by copying the original db
test_db = DAL('sqlite://testing.sqlite')
for tablename in db.tables: # Copy tables!
table_copy = [copy(f) for f in db[tablename]]
test_db.define_table(tablename, *table_copy)
db=test_dbI moved this code to the testrunner script to make the testing less intrusive for the application code. And I added code to use a fixture file which fill the database so that we have a fixed state to start with for each test.
Integrating the UI tests in the script proved to be a little hard. Running a script in web2py, in this case the testRunner script with the -M parameter makes the code of the web2py model files available to the script and indirectly to the unit and doctests. But running a UI test means simulating a browser-interaction, and that can only be another web2py instance.
Normally this new instance of our app will use the standard(production) database, but that not what we want. We need to signal, one way or another that this instance should use the testing db. I solved it by using an environment var WEB2PY_TESTING_DB. When set, the database definitions in the model file switches to the testing db and fills it using the fixture file. The last step is not strictly necessary because testRunner.py already initialized the db, but we want to make sure the fixture is fresh.
if os.getenv('WEB2PY_USE_DB_TESTING')
db = DAL('sqlite://testing.sqlite')
if os.getenv('WEB2PY_USE_DB_TESTING') and not
os.getenv('WEB2PY_DB_TESTING_FILLED'):
# use fixture file only once during lifetime of the app
import yaml
for table in db.tables:
# Make sure to cascade, or this will fail
# for tables that have FK references.
db[table].truncate("CASCADE")
data = yaml.load(open('applications/%s/private/testfixture.yaml'%current.request.application))
for table_name, rows in data.items():
for r in rows:
db[table_name].insert(**r)
os.environ['WEB2PY_DB_TESTING_FILLED']='TRUE'
Excerpt from the fixture file, see the pyyaml and yaml documentation for details.
activity:
- name: 1. No workshops
nr_prefs: 0
nr_sessions: 0
amount: 100
w_start: !!timestamp '2021-01-02 10:00:00'
w_end: !!timestamp '2021-01-02 17:00:00'
w_register: !!timestamp '2021-01-02 10:00:00'
- name: 2. No workshops, no prefs, only options
nr_prefs: 0
nr_sessions: 1
amount: 200
w_start: !!timestamp '2021-01-02 10:00:00'
w_end: !!timestamp '2021-01-02 17:00:00'
w_register: !!timestamp '2021-01-02 10:00:00'
workshop:
- name: act1, ws1
activity: 1
number: 1
- name: act1, ws2
activity: 1
number: 2
But wait, when we use sqlite, the database file is already in use in our testing environment, so we get a database locked error. One way of solving this is to close the database connection in our script after running the doc- and unittests like this.
...
unittest.TextTestRunner(verbosity=2).run(suite) # run the doctests and unittests
db._adapter.connection.close()
unittest.TextTestRunner(verbosity=2).run(ui_suite) # run the ui tests
...
""" Unittest for controller x
"""
import unittest
import cPickle as pickle
from gluon import *
from gluon.contrib.test_helpers import form_postvars
from gluon import current
import components
class AanmeldingController(unittest.TestCase):
""" test CRUD for contact, participant using the classes from module components"""
def __init__(self, p):
global auth, session, request
unittest.TestCase.__init__(self, p)
self.session = pickle.dumps(current.session)
current.request.application = 'dibsa'
current.request.controller = 'default'
self.request = pickle.dumps(current.request)
self.part1=Storage(firstname="Nico",
prefix="de",
lastname="Groot",
option1=2,
)
self.partpref=Storage(firstname="Nico",
prefix="de",
lastname="Groot",
pref0=1,
)
self.assertTrue(current.app.db._uri, 'sqlite://testing.sqlite')
self.resetDB()
def setUp(self):
global response, session, request, auth
current.session = pickle.loads(self.session)
current.request = pickle.loads(self.request)
#preconditions
def resetDB(self):
""""Start with a fixture, read from yaml file, first empty db table"""
import yaml
for table in db.tables:
# Make sure to cascade, or this will fail
# for tables that have FK references.
db[table].truncate("CASCADE")
data = yaml.load(open('applications/%s/private/testfixture.yaml'%current.request.application))
for table_name, rows in data.items():
for r in rows:
db[table_name].insert(
**r
)
def testContactAutopay(self):
""" create and save contact, autopay
"""
setCurrentActivityId(2)
self._contact_autopay(True)
def _contact_autopay(self, autopay=True):
""" create and save contact, autopay, not a participant
returns contactid
"""
# Register a user in the db
cp=components.newClient()
contact=Storage({"firstname":"Nico",
"prefix":"de",
"lastname":"Groot",
"account":"123"})
cp.form.vars=contact
cp.manualsave(autopay=autopay) # save in db.person and in db.p2a
# save ok?
found=db((db.person.id==cp.id)).select(db.person.ALL)[0]
self.assertTrue(cp.id==found.id) # id ok
# all other fields
for key in contact.keys():
#print key, contact[key]
self.assertTrue(found[key]==contact[key]) # field ok
found=db( (db.person2activity.person==cp.id)&
(db.person2activity.autopay==autopay)&
(db.person2activity.pclass==current.app.constants.CONTACT)&
(db.person2activity.activity==current.app.activity.id)
).select(db.person2activity.person)[0].person
self.assertTrue(cp.id==found.id)
return cp.id
UI tests
Another incomplete example to get you started.
""" Uses Selenium2/webdriver to test the UI of DIBSA
Version 0.9:
contain tests for aanmelding/registration pages
"""
import unittest, time, re
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.webdriver import ActionChains
from util import Browser
class Aanmelding(unittest.TestCase):
def setUp(self):
"""test using Chrome"""
self.verificationErrors = []
self.browser = webdriver.Chrome()
self.action_chains = ActionChains(self.browser)
def _fill_name(self):
self.browser.find_element_by_name("company").send_keys("TST")
self.browser.find_element_by_name("firstname").send_keys("N.C.")
self.browser.find_element_by_name("prefix").send_keys("de")
self.browser.find_element_by_name("lastname").send_keys("Groot")
el = self.browser.find_element_by_id("person_gender")
el.send_keys("M")
def test_xxx(self):
self._fill_name()
el = self.browser.find_element_by_id("pref0")
self.assertEqual(el.text,'option1option2',"prefs not ok: %s"%el.text)
Running the tests
The script is run on the command line as follows
Documentation
http://www.web2pyslices.com/slices/take_slice/67
http://www.web2pyslices.com/slices/take_slice/142 (needs an update)
Virtualenv slides
# use fixture file only once during lifetime of the app
import yaml
for table in db.tables:
# Make sure to cascade, or this will fail
# for tables that have FK references.
db[table].truncate("CASCADE")
data = yaml.load(open('applications/%s/private/testfixture.yaml'%current.request.application))
for table_name, rows in data.items():
for r in rows:
db[table_name].insert(**r)
os.environ['WEB2PY_DB_TESTING_FILLED']='TRUE'
Excerpt from the fixture file, see the pyyaml and yaml documentation for details.
activity:
- name: 1. No workshops
nr_prefs: 0
nr_sessions: 0
amount: 100
w_start: !!timestamp '2021-01-02 10:00:00'
w_end: !!timestamp '2021-01-02 17:00:00'
w_register: !!timestamp '2021-01-02 10:00:00'
- name: 2. No workshops, no prefs, only options
nr_prefs: 0
nr_sessions: 1
amount: 200
w_start: !!timestamp '2021-01-02 10:00:00'
w_end: !!timestamp '2021-01-02 17:00:00'
w_register: !!timestamp '2021-01-02 10:00:00'
workshop:
- name: act1, ws1
activity: 1
number: 1
- name: act1, ws2
activity: 1
number: 2
But wait, when we use sqlite, the database file is already in use in our testing environment, so we get a database locked error. One way of solving this is to close the database connection in our script after running the doc- and unittests like this.
...
unittest.TextTestRunner(verbosity=2).run(suite) # run the doctests and unittests
db._adapter.connection.close()
unittest.TextTestRunner(verbosity=2).run(ui_suite) # run the ui tests
...
Preparation
- Put the script in the root of your web2py instance
- Add the helper to the gluon\contrib folder
- Inside your application folder, create a folder tests with subfolders controller and model
- If you want to use UI testing install selenium using pip install selenium, see http://ncdegroot.blogspot.com/2011/06/using-selenium-2-with-python-and-web2py.html and create an userinterface folder inside tests
Unittest
An incomplete example. For an introduction see http://agiletesting.blogspot.com/2005/01/python-unit-testing-part-1-unittest.html""" Unittest for controller x
"""
import unittest
import cPickle as pickle
from gluon import *
from gluon.contrib.test_helpers import form_postvars
from gluon import current
import components
class AanmeldingController(unittest.TestCase):
""" test CRUD for contact, participant using the classes from module components"""
def __init__(self, p):
global auth, session, request
unittest.TestCase.__init__(self, p)
self.session = pickle.dumps(current.session)
current.request.application = 'dibsa'
current.request.controller = 'default'
self.request = pickle.dumps(current.request)
self.part1=Storage(firstname="Nico",
prefix="de",
lastname="Groot",
option1=2,
)
self.partpref=Storage(firstname="Nico",
prefix="de",
lastname="Groot",
pref0=1,
)
self.assertTrue(current.app.db._uri, 'sqlite://testing.sqlite')
self.resetDB()
def setUp(self):
global response, session, request, auth
current.session = pickle.loads(self.session)
current.request = pickle.loads(self.request)
#preconditions
def resetDB(self):
""""Start with a fixture, read from yaml file, first empty db table"""
import yaml
for table in db.tables:
# Make sure to cascade, or this will fail
# for tables that have FK references.
db[table].truncate("CASCADE")
data = yaml.load(open('applications/%s/private/testfixture.yaml'%current.request.application))
for table_name, rows in data.items():
for r in rows:
db[table_name].insert(
**r
)
def testContactAutopay(self):
""" create and save contact, autopay
"""
setCurrentActivityId(2)
self._contact_autopay(True)
def _contact_autopay(self, autopay=True):
""" create and save contact, autopay, not a participant
returns contactid
"""
# Register a user in the db
cp=components.newClient()
contact=Storage({"firstname":"Nico",
"prefix":"de",
"lastname":"Groot",
"account":"123"})
cp.form.vars=contact
cp.manualsave(autopay=autopay) # save in db.person and in db.p2a
# save ok?
found=db((db.person.id==cp.id)).select(db.person.ALL)[0]
self.assertTrue(cp.id==found.id) # id ok
# all other fields
for key in contact.keys():
#print key, contact[key]
self.assertTrue(found[key]==contact[key]) # field ok
found=db( (db.person2activity.person==cp.id)&
(db.person2activity.autopay==autopay)&
(db.person2activity.pclass==current.app.constants.CONTACT)&
(db.person2activity.activity==current.app.activity.id)
).select(db.person2activity.person)[0].person
self.assertTrue(cp.id==found.id)
return cp.id
Doctests
You can add doctests to your controllers as described in the web2py book. You can already run your doctests using the admin interface or from the commandline. Nothing new here, except that a separate testing database is used with the fixture file. An example: >>> current.app.db._uri # make sure it IS the testing db
'sqlite://testing.sqlite'
>>> current.app.activity.name
'1. No workshops'
>>> current.app.activity.amount
100.0
>>> # case 1: create contact, save to db, check its there and delete it (and check if it is deleted)
>>> cp=components.newClient()
>>> cp.form.vars={"firstname":"nico","account":"123"}
>>> cp.manualsave(autopay=False)
>>> cp.id==db((db.person2activity.person==cp.id)&(db.person2activity.autopay==False)&(db.person2activity.activity==current.app.activity.id)).select(db.person2activity.person)[0].person
True
UI tests
Another incomplete example to get you started.
""" Uses Selenium2/webdriver to test the UI of DIBSA
Version 0.9:
contain tests for aanmelding/registration pages
"""
import unittest, time, re
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.webdriver import ActionChains
from util import Browser
class Aanmelding(unittest.TestCase):
def setUp(self):
"""test using Chrome"""
self.verificationErrors = []
self.browser = webdriver.Chrome()
self.action_chains = ActionChains(self.browser)
def _fill_name(self):
self.browser.find_element_by_name("company").send_keys("TST")
self.browser.find_element_by_name("firstname").send_keys("N.C.")
self.browser.find_element_by_name("prefix").send_keys("de")
self.browser.find_element_by_name("lastname").send_keys("Groot")
el = self.browser.find_element_by_id("person_gender")
el.send_keys("M")
def test_xxx(self):
self._fill_name()
el = self.browser.find_element_by_id("pref0")
self.assertEqual(el.text,'option1option2',"prefs not ok: %s"%el.text)
The script is run on the command line as follows
python web2py.py -S dibsa -M -R testRunner.pyThe parameter after -S specifies the application name, -M makes sure the model files are used, and -R specifies our script.
http://www.web2pyslices.com/slices/take_slice/67
http://www.web2pyslices.com/slices/take_slice/142 (needs an update)
Virtualenv slides