Adding Tests¶
We will now add tests for the models and views as well as a few functional
tests in a new tests
subpackage. Tests ensure that an application works,
and that it continues to work when changes are made in the future.
The file tests.py
was generated as part of the alchemy
scaffold, but it
is a common practice to put tests into a tests
subpackage, especially as
projects grow in size and complexity. Each module in the test subpackage
should contain tests for its corresponding module in our application. Each
corresponding pair of modules should have the same names, except the test
module should have the prefix test_
.
Start by deleting tests.py
, then create a new directory to contain our new
tests as well as a new empty file tests/__init__.py
.
Warning
It is very important when refactoring a Python module into a package to be
sure to delete the cache files (.pyc
files or __pycache__
folders)
sitting around! Python will prioritize the cache files before traversing
into folders, using the old code, and you will wonder why none of your
changes are working!
Test the views¶
We'll create a new tests/test_views.py
file, adding a BaseTest
class
used as the base for other test classes. Next we'll add tests for each view
function we previously added to our application. We'll add four test classes:
ViewWikiTests
, ViewPageTests
, AddPageTests
, and EditPageTests
.
These test the view_wiki
, view_page
, add_page
, and edit_page
views.
Functional tests¶
We'll test the whole application, covering security aspects that are not tested
in the unit tests, like logging in, logging out, checking that the basic
user cannot edit pages that it didn't create but the editor
user can, and
so on.
View the results of all our edits to tests
subpackage¶
Open tutorial/tests/test_views.py
, and edit it such that it appears as
follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 | import unittest
import transaction
from pyramid import testing
def dummy_request(dbsession):
return testing.DummyRequest(dbsession=dbsession)
class BaseTest(unittest.TestCase):
def setUp(self):
from ..models import get_tm_session
self.config = testing.setUp(settings={
'sqlalchemy.url': 'sqlite:///:memory:'
})
self.config.include('..models')
self.config.include('..routes')
session_factory = self.config.registry['dbsession_factory']
self.session = get_tm_session(session_factory, transaction.manager)
self.init_database()
def init_database(self):
from ..models.meta import Base
session_factory = self.config.registry['dbsession_factory']
engine = session_factory.kw['bind']
Base.metadata.create_all(engine)
def tearDown(self):
testing.tearDown()
transaction.abort()
def makeUser(self, name, role, password='dummy'):
from ..models import User
user = User(name=name, role=role)
user.set_password(password)
return user
def makePage(self, name, data, creator):
from ..models import Page
return Page(name=name, data=data, creator=creator)
class ViewWikiTests(unittest.TestCase):
def setUp(self):
self.config = testing.setUp()
self.config.include('..routes')
def tearDown(self):
testing.tearDown()
def _callFUT(self, request):
from tutorial.views.default import view_wiki
return view_wiki(request)
def test_it(self):
request = testing.DummyRequest()
response = self._callFUT(request)
self.assertEqual(response.location, 'http://example.com/FrontPage')
class ViewPageTests(BaseTest):
def _callFUT(self, request):
from tutorial.views.default import view_page
return view_page(request)
def test_it(self):
from ..routes import PageResource
# add a page to the db
user = self.makeUser('foo', 'editor')
page = self.makePage('IDoExist', 'Hello CruelWorld IDoExist', user)
self.session.add_all([page, user])
# create a request asking for the page we've created
request = dummy_request(self.session)
request.context = PageResource(page)
# call the view we're testing and check its behavior
info = self._callFUT(request)
self.assertEqual(info['page'], page)
self.assertEqual(
info['content'],
'<div class="document">\n'
'<p>Hello <a href="http://example.com/add_page/CruelWorld">'
'CruelWorld</a> '
'<a href="http://example.com/IDoExist">'
'IDoExist</a>'
'</p>\n</div>\n')
self.assertEqual(info['edit_url'],
'http://example.com/IDoExist/edit_page')
class AddPageTests(BaseTest):
def _callFUT(self, request):
from tutorial.views.default import add_page
return add_page(request)
def test_it_pageexists(self):
from ..models import Page
from ..routes import NewPage
request = testing.DummyRequest({'form.submitted': True,
'body': 'Hello yo!'},
dbsession=self.session)
request.user = self.makeUser('foo', 'editor')
request.context = NewPage('AnotherPage')
self._callFUT(request)
pagecount = self.session.query(Page).filter_by(name='AnotherPage').count()
self.assertGreater(pagecount, 0)
def test_it_notsubmitted(self):
from ..routes import NewPage
request = dummy_request(self.session)
request.user = self.makeUser('foo', 'editor')
request.context = NewPage('AnotherPage')
info = self._callFUT(request)
self.assertEqual(info['pagedata'], '')
self.assertEqual(info['save_url'],
'http://example.com/add_page/AnotherPage')
def test_it_submitted(self):
from ..models import Page
from ..routes import NewPage
request = testing.DummyRequest({'form.submitted': True,
'body': 'Hello yo!'},
dbsession=self.session)
request.user = self.makeUser('foo', 'editor')
request.context = NewPage('AnotherPage')
self._callFUT(request)
page = self.session.query(Page).filter_by(name='AnotherPage').one()
self.assertEqual(page.data, 'Hello yo!')
class EditPageTests(BaseTest):
def _callFUT(self, request):
from tutorial.views.default import edit_page
return edit_page(request)
def makeContext(self, page):
from ..routes import PageResource
return PageResource(page)
def test_it_notsubmitted(self):
user = self.makeUser('foo', 'editor')
page = self.makePage('abc', 'hello', user)
self.session.add_all([page, user])
request = dummy_request(self.session)
request.context = self.makeContext(page)
info = self._callFUT(request)
self.assertEqual(info['pagename'], 'abc')
self.assertEqual(info['save_url'],
'http://example.com/abc/edit_page')
def test_it_submitted(self):
user = self.makeUser('foo', 'editor')
page = self.makePage('abc', 'hello', user)
self.session.add_all([page, user])
request = testing.DummyRequest({'form.submitted': True,
'body': 'Hello yo!'},
dbsession=self.session)
request.context = self.makeContext(page)
response = self._callFUT(request)
self.assertEqual(response.location, 'http://example.com/abc')
self.assertEqual(page.data, 'Hello yo!')
|
Open tutorial/tests/test_functional.py
, and edit it such that it appears as
follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | import transaction
import unittest
import webtest
class FunctionalTests(unittest.TestCase):
basic_login = (
'/login?login=basic&password=basic'
'&next=FrontPage&form.submitted=Login')
basic_wrong_login = (
'/login?login=basic&password=incorrect'
'&next=FrontPage&form.submitted=Login')
editor_login = (
'/login?login=editor&password=editor'
'&next=FrontPage&form.submitted=Login')
@classmethod
def setUpClass(cls):
from tutorial.models.meta import Base
from tutorial.models import (
User,
Page,
get_tm_session,
)
from tutorial import main
settings = {
'sqlalchemy.url': 'sqlite://',
'auth.secret': 'seekrit',
}
app = main({}, **settings)
cls.testapp = webtest.TestApp(app)
session_factory = app.registry['dbsession_factory']
cls.engine = session_factory.kw['bind']
Base.metadata.create_all(bind=cls.engine)
with transaction.manager:
dbsession = get_tm_session(session_factory, transaction.manager)
editor = User(name='editor', role='editor')
editor.set_password('editor')
basic = User(name='basic', role='basic')
basic.set_password('basic')
page1 = Page(name='FrontPage', data='This is the front page')
page1.creator = editor
page2 = Page(name='BackPage', data='This is the back page')
page2.creator = basic
dbsession.add_all([basic, editor, page1, page2])
@classmethod
def tearDownClass(cls):
from tutorial.models.meta import Base
Base.metadata.drop_all(bind=cls.engine)
def test_root(self):
res = self.testapp.get('/', status=302)
self.assertEqual(res.location, 'http://localhost/FrontPage')
def test_FrontPage(self):
res = self.testapp.get('/FrontPage', status=200)
self.assertTrue(b'FrontPage' in res.body)
def test_unexisting_page(self):
self.testapp.get('/SomePage', status=404)
def test_successful_log_in(self):
res = self.testapp.get(self.basic_login, status=302)
self.assertEqual(res.location, 'http://localhost/FrontPage')
def test_failed_log_in(self):
res = self.testapp.get(self.basic_wrong_login, status=200)
self.assertTrue(b'login' in res.body)
def test_logout_link_present_when_logged_in(self):
self.testapp.get(self.basic_login, status=302)
res = self.testapp.get('/FrontPage', status=200)
self.assertTrue(b'Logout' in res.body)
def test_logout_link_not_present_after_logged_out(self):
self.testapp.get(self.basic_login, status=302)
self.testapp.get('/FrontPage', status=200)
res = self.testapp.get('/logout', status=302)
self.assertTrue(b'Logout' not in res.body)
def test_anonymous_user_cannot_edit(self):
res = self.testapp.get('/FrontPage/edit_page', status=302).follow()
self.assertTrue(b'Login' in res.body)
def test_anonymous_user_cannot_add(self):
res = self.testapp.get('/add_page/NewPage', status=302).follow()
self.assertTrue(b'Login' in res.body)
def test_basic_user_cannot_edit_front(self):
self.testapp.get(self.basic_login, status=302)
res = self.testapp.get('/FrontPage/edit_page', status=302).follow()
self.assertTrue(b'Login' in res.body)
def test_basic_user_can_edit_back(self):
self.testapp.get(self.basic_login, status=302)
res = self.testapp.get('/BackPage/edit_page', status=200)
self.assertTrue(b'Editing' in res.body)
def test_basic_user_can_add(self):
self.testapp.get(self.basic_login, status=302)
res = self.testapp.get('/add_page/NewPage', status=200)
self.assertTrue(b'Editing' in res.body)
def test_editors_member_user_can_edit(self):
self.testapp.get(self.editor_login, status=302)
res = self.testapp.get('/FrontPage/edit_page', status=200)
self.assertTrue(b'Editing' in res.body)
def test_editors_member_user_can_add(self):
self.testapp.get(self.editor_login, status=302)
res = self.testapp.get('/add_page/NewPage', status=200)
self.assertTrue(b'Editing' in res.body)
def test_editors_member_user_can_view(self):
self.testapp.get(self.editor_login, status=302)
res = self.testapp.get('/FrontPage', status=200)
self.assertTrue(b'FrontPage' in res.body)
|
Note
We're utilizing the excellent WebTest package to do functional testing of
the application. This is defined in the tests_require
section of our
setup.py
. Any other dependencies needed only for testing purposes can be
added there and will be installed automatically when running
setup.py test
.
Running the tests¶
We can run these tests similarly to how we did in Run the tests:
On UNIX:
$ $VENV/bin/py.test -q
On Windows:
c:\pyramidtut\tutorial> %VENV%\Scripts\py.test -q
The expected result should look like the following:
......................
22 passed, 1 pytest-warnings in 5.81 seconds
Note
If you use Python 3 during this tutorial, you will see deprecation warnings in the output, which we will choose to ignore. In making this tutorial run on both Python 2 and 3, the authors prioritized simplicity and focus for the learner over accommodating warnings. In your own app or as extra credit, you may choose to either drop Python 2 support or hack your code to work without warnings on both Python 2 and 3.