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 from choosing the sqlalchemy
backend
option, 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¶
Create tutorial/tests/test_views.py
such that it appears as follows:
1import unittest
2import transaction
3
4from pyramid import testing
5
6
7def dummy_request(dbsession):
8 return testing.DummyRequest(dbsession=dbsession)
9
10
11class BaseTest(unittest.TestCase):
12 def setUp(self):
13 from ..models import get_tm_session
14 self.config = testing.setUp(settings={
15 'sqlalchemy.url': 'sqlite:///:memory:'
16 })
17 self.config.include('..models')
18 self.config.include('..routes')
19
20 session_factory = self.config.registry['dbsession_factory']
21 self.session = get_tm_session(session_factory, transaction.manager)
22
23 self.init_database()
24
25 def init_database(self):
26 from ..models.meta import Base
27 session_factory = self.config.registry['dbsession_factory']
28 engine = session_factory.kw['bind']
29 Base.metadata.create_all(engine)
30
31 def tearDown(self):
32 testing.tearDown()
33 transaction.abort()
34
35 def makeUser(self, name, role, password='dummy'):
36 from ..models import User
37 user = User(name=name, role=role)
38 user.set_password(password)
39 return user
40
41 def makePage(self, name, data, creator):
42 from ..models import Page
43 return Page(name=name, data=data, creator=creator)
44
45
46class ViewWikiTests(unittest.TestCase):
47 def setUp(self):
48 self.config = testing.setUp()
49 self.config.include('..routes')
50
51 def tearDown(self):
52 testing.tearDown()
53
54 def _callFUT(self, request):
55 from tutorial.views.default import view_wiki
56 return view_wiki(request)
57
58 def test_it(self):
59 request = testing.DummyRequest()
60 response = self._callFUT(request)
61 self.assertEqual(response.location, 'http://example.com/FrontPage')
62
63
64class ViewPageTests(BaseTest):
65 def _callFUT(self, request):
66 from tutorial.views.default import view_page
67 return view_page(request)
68
69 def test_it(self):
70 from ..routes import PageResource
71
72 # add a page to the db
73 user = self.makeUser('foo', 'editor')
74 page = self.makePage('IDoExist', 'Hello CruelWorld IDoExist', user)
75 self.session.add_all([page, user])
76
77 # create a request asking for the page we've created
78 request = dummy_request(self.session)
79 request.context = PageResource(page)
80
81 # call the view we're testing and check its behavior
82 info = self._callFUT(request)
83 self.assertEqual(info['page'], page)
84 self.assertEqual(
85 info['content'],
86 '<div class="document">\n'
87 '<p>Hello <a href="http://example.com/add_page/CruelWorld">'
88 'CruelWorld</a> '
89 '<a href="http://example.com/IDoExist">'
90 'IDoExist</a>'
91 '</p>\n</div>\n')
92 self.assertEqual(info['edit_url'],
93 'http://example.com/IDoExist/edit_page')
94
95
96class AddPageTests(BaseTest):
97 def _callFUT(self, request):
98 from tutorial.views.default import add_page
99 return add_page(request)
100
101 def test_it_pageexists(self):
102 from ..models import Page
103 from ..routes import NewPage
104 request = testing.DummyRequest({'form.submitted': True,
105 'body': 'Hello yo!'},
106 dbsession=self.session)
107 request.user = self.makeUser('foo', 'editor')
108 request.context = NewPage('AnotherPage')
109 self._callFUT(request)
110 pagecount = self.session.query(Page).filter_by(name='AnotherPage').count()
111 self.assertGreater(pagecount, 0)
112
113 def test_it_notsubmitted(self):
114 from ..routes import NewPage
115 request = dummy_request(self.session)
116 request.user = self.makeUser('foo', 'editor')
117 request.context = NewPage('AnotherPage')
118 info = self._callFUT(request)
119 self.assertEqual(info['pagedata'], '')
120 self.assertEqual(info['save_url'],
121 'http://example.com/add_page/AnotherPage')
122
123 def test_it_submitted(self):
124 from ..models import Page
125 from ..routes import NewPage
126 request = testing.DummyRequest({'form.submitted': True,
127 'body': 'Hello yo!'},
128 dbsession=self.session)
129 request.user = self.makeUser('foo', 'editor')
130 request.context = NewPage('AnotherPage')
131 self._callFUT(request)
132 page = self.session.query(Page).filter_by(name='AnotherPage').one()
133 self.assertEqual(page.data, 'Hello yo!')
134
135
136class EditPageTests(BaseTest):
137 def _callFUT(self, request):
138 from tutorial.views.default import edit_page
139 return edit_page(request)
140
141 def makeContext(self, page):
142 from ..routes import PageResource
143 return PageResource(page)
144
145 def test_it_notsubmitted(self):
146 user = self.makeUser('foo', 'editor')
147 page = self.makePage('abc', 'hello', user)
148 self.session.add_all([page, user])
149
150 request = dummy_request(self.session)
151 request.context = self.makeContext(page)
152 info = self._callFUT(request)
153 self.assertEqual(info['pagename'], 'abc')
154 self.assertEqual(info['save_url'],
155 'http://example.com/abc/edit_page')
156
157 def test_it_submitted(self):
158 user = self.makeUser('foo', 'editor')
159 page = self.makePage('abc', 'hello', user)
160 self.session.add_all([page, user])
161
162 request = testing.DummyRequest({'form.submitted': True,
163 'body': 'Hello yo!'},
164 dbsession=self.session)
165 request.context = self.makeContext(page)
166 response = self._callFUT(request)
167 self.assertEqual(response.location, 'http://example.com/abc')
168 self.assertEqual(page.data, 'Hello yo!')
Create tutorial/tests/test_functional.py
such that it appears as follows:
1import transaction
2import unittest
3import webtest
4
5
6class FunctionalTests(unittest.TestCase):
7
8 basic_login = (
9 '/login?login=basic&password=basic'
10 '&next=FrontPage&form.submitted=Login')
11 basic_wrong_login = (
12 '/login?login=basic&password=incorrect'
13 '&next=FrontPage&form.submitted=Login')
14 basic_login_no_next = (
15 '/login?login=basic&password=basic'
16 '&form.submitted=Login')
17 editor_login = (
18 '/login?login=editor&password=editor'
19 '&next=FrontPage&form.submitted=Login')
20
21 @classmethod
22 def setUpClass(cls):
23 from tutorial.models.meta import Base
24 from tutorial.models import (
25 User,
26 Page,
27 get_tm_session,
28 )
29 from tutorial import main
30
31 settings = {
32 'sqlalchemy.url': 'sqlite://',
33 'auth.secret': 'seekrit',
34 }
35 app = main({}, **settings)
36 cls.testapp = webtest.TestApp(app)
37
38 session_factory = app.registry['dbsession_factory']
39 cls.engine = session_factory.kw['bind']
40 Base.metadata.create_all(bind=cls.engine)
41
42 with transaction.manager:
43 dbsession = get_tm_session(session_factory, transaction.manager)
44 editor = User(name='editor', role='editor')
45 editor.set_password('editor')
46 basic = User(name='basic', role='basic')
47 basic.set_password('basic')
48 page1 = Page(name='FrontPage', data='This is the front page')
49 page1.creator = editor
50 page2 = Page(name='BackPage', data='This is the back page')
51 page2.creator = basic
52 dbsession.add_all([basic, editor, page1, page2])
53
54 @classmethod
55 def tearDownClass(cls):
56 from tutorial.models.meta import Base
57 Base.metadata.drop_all(bind=cls.engine)
58
59 def test_root(self):
60 res = self.testapp.get('/', status=302)
61 self.assertEqual(res.location, 'http://localhost/FrontPage')
62
63 def test_FrontPage(self):
64 res = self.testapp.get('/FrontPage', status=200)
65 self.assertTrue(b'FrontPage' in res.body)
66
67 def test_unexisting_page(self):
68 self.testapp.get('/SomePage', status=404)
69
70 def test_successful_log_in(self):
71 res = self.testapp.get(self.basic_login, status=302)
72 self.assertEqual(res.location, 'http://localhost/FrontPage')
73
74 def test_successful_log_in_no_next(self):
75 res = self.testapp.get(self.basic_login_no_next, status=302)
76 self.assertEqual(res.location, 'http://localhost/')
77
78 def test_failed_log_in(self):
79 res = self.testapp.get(self.basic_wrong_login, status=200)
80 self.assertTrue(b'login' in res.body)
81
82 def test_logout_link_present_when_logged_in(self):
83 self.testapp.get(self.basic_login, status=302)
84 res = self.testapp.get('/FrontPage', status=200)
85 self.assertTrue(b'Logout' in res.body)
86
87 def test_logout_link_not_present_after_logged_out(self):
88 self.testapp.get(self.basic_login, status=302)
89 self.testapp.get('/FrontPage', status=200)
90 res = self.testapp.get('/logout', status=302)
91 self.assertTrue(b'Logout' not in res.body)
92
93 def test_anonymous_user_cannot_edit(self):
94 res = self.testapp.get('/FrontPage/edit_page', status=302).follow()
95 self.assertTrue(b'Login' in res.body)
96
97 def test_anonymous_user_cannot_add(self):
98 res = self.testapp.get('/add_page/NewPage', status=302).follow()
99 self.assertTrue(b'Login' in res.body)
100
101 def test_basic_user_cannot_edit_front(self):
102 self.testapp.get(self.basic_login, status=302)
103 res = self.testapp.get('/FrontPage/edit_page', status=302).follow()
104 self.assertTrue(b'Login' in res.body)
105
106 def test_basic_user_can_edit_back(self):
107 self.testapp.get(self.basic_login, status=302)
108 res = self.testapp.get('/BackPage/edit_page', status=200)
109 self.assertTrue(b'Editing' in res.body)
110
111 def test_basic_user_can_add(self):
112 self.testapp.get(self.basic_login, status=302)
113 res = self.testapp.get('/add_page/NewPage', status=200)
114 self.assertTrue(b'Editing' in res.body)
115
116 def test_editors_member_user_can_edit(self):
117 self.testapp.get(self.editor_login, status=302)
118 res = self.testapp.get('/FrontPage/edit_page', status=200)
119 self.assertTrue(b'Editing' in res.body)
120
121 def test_editors_member_user_can_add(self):
122 self.testapp.get(self.editor_login, status=302)
123 res = self.testapp.get('/add_page/NewPage', status=200)
124 self.assertTrue(b'Editing' in res.body)
125
126 def test_editors_member_user_can_view(self):
127 self.testapp.get(self.editor_login, status=302)
128 res = self.testapp.get('/FrontPage', status=200)
129 self.assertTrue(b'FrontPage' in res.body)
130
131 def test_redirect_to_edit_for_existing_page(self):
132 self.testapp.get(self.editor_login, status=302)
133 res = self.testapp.get('/add_page/FrontPage', status=302)
134 self.assertTrue(b'FrontPage' in res.body)
Create tutorial/tests/test_initdb.py
such that it appears as follows:
1import os
2import unittest
3
4
5class TestInitializeDB(unittest.TestCase):
6
7 def test_usage(self):
8 from ..scripts.initialize_db import main
9 with self.assertRaises(SystemExit):
10 main(argv=['foo'])
11
12 def test_run(self):
13 from ..scripts.initialize_db import main
14 main(argv=['foo', 'development.ini'])
15 self.assertTrue(os.path.exists('tutorial.sqlite'))
16 os.remove('tutorial.sqlite')
Create tutorial/tests/test_security.py
such that it appears as follows:
1import unittest
2from pyramid.testing import DummyRequest
3
4
5class TestMyAuthenticationPolicy(unittest.TestCase):
6
7 def test_no_user(self):
8 request = DummyRequest()
9 request.user = None
10
11 from ..security import MyAuthenticationPolicy
12 policy = MyAuthenticationPolicy(None)
13 self.assertEqual(policy.authenticated_userid(request), None)
14
15 def test_authenticated_user(self):
16 from ..models import User
17 request = DummyRequest()
18 request.user = User()
19 request.user.id = 'foo'
20
21 from ..security import MyAuthenticationPolicy
22 policy = MyAuthenticationPolicy(None)
23 self.assertEqual(policy.authenticated_userid(request), 'foo')
Create tutorial/tests/test_user_model.py
such that it appears as follows:
1import unittest
2import transaction
3
4from pyramid import testing
5
6
7class BaseTest(unittest.TestCase):
8
9 def setUp(self):
10 from ..models import get_tm_session
11 self.config = testing.setUp(settings={
12 'sqlalchemy.url': 'sqlite:///:memory:'
13 })
14 self.config.include('..models')
15 self.config.include('..routes')
16
17 session_factory = self.config.registry['dbsession_factory']
18 self.session = get_tm_session(session_factory, transaction.manager)
19
20 self.init_database()
21
22 def init_database(self):
23 from ..models.meta import Base
24 session_factory = self.config.registry['dbsession_factory']
25 engine = session_factory.kw['bind']
26 Base.metadata.create_all(engine)
27
28 def tearDown(self):
29 testing.tearDown()
30 transaction.abort()
31
32 def makeUser(self, name, role):
33 from ..models import User
34 return User(name=name, role=role)
35
36
37class TestSetPassword(BaseTest):
38
39 def test_password_hash_saved(self):
40 user = self.makeUser(name='foo', role='bar')
41 self.assertFalse(user.password_hash)
42
43 user.set_password('secret')
44 self.assertTrue(user.password_hash)
45
46
47class TestCheckPassword(BaseTest):
48
49 def test_password_hash_not_set(self):
50 user = self.makeUser(name='foo', role='bar')
51 self.assertFalse(user.password_hash)
52
53 self.assertFalse(user.check_password('secret'))
54
55 def test_correct_password(self):
56 user = self.makeUser(name='foo', role='bar')
57 user.set_password('secret')
58 self.assertTrue(user.password_hash)
59
60 self.assertTrue(user.check_password('secret'))
61
62 def test_incorrect_password(self):
63 user = self.makeUser(name='foo', role='bar')
64 user.set_password('secret')
65 self.assertTrue(user.password_hash)
66
67 self.assertFalse(user.check_password('incorrect'))
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, but first delete the SQLite database tutorial.sqlite
. If you do not delete the database, then you will see an integrity error when running the tests.
On Unix:
rm tutorial.sqlite
$VENV/bin/pytest -q
On Windows:
del tutorial.sqlite
%VENV%\Scripts\pytest -q
The expected result should look like the following:
................................
32 passed in 9.90 seconds