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