diff options
Diffstat (limited to 'docs/tutorials/wiki')
| -rw-r--r-- | docs/tutorials/wiki/authorization.rst | 218 | ||||
| -rw-r--r-- | docs/tutorials/wiki/definingmodels.rst | 70 | ||||
| -rw-r--r-- | docs/tutorials/wiki/definingviews.rst | 45 | ||||
| -rw-r--r-- | docs/tutorials/wiki/index.rst | 1 | ||||
| -rw-r--r-- | docs/tutorials/wiki/src/tests/tutorial/tests.py | 216 | ||||
| -rw-r--r-- | docs/tutorials/wiki/tests.rst | 78 |
6 files changed, 403 insertions, 225 deletions
diff --git a/docs/tutorials/wiki/authorization.rst b/docs/tutorials/wiki/authorization.rst index ee86eb543..e4480d6d9 100644 --- a/docs/tutorials/wiki/authorization.rst +++ b/docs/tutorials/wiki/authorization.rst @@ -7,21 +7,25 @@ edit, and add pages to our wiki. For purposes of demonstration we'll change our application to allow people whom are members of a *group* named ``group:editors`` to add and edit wiki pages but we'll continue allowing anyone with access to the server to view pages. :app:`Pyramid` provides -facilities for *authorization* and *authentication*. We'll make use of both -features to provide security to our application. +facilities for :term:`authorization` and :term:`authentication`. We'll make +use of both features to provide security to our application. -The source code for this tutorial stage can be browsed via -`http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki/src/authorization/ -<http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki/src/authorization/>`_. +We will add an :term:`authentication policy` and an +:term:`authorization policy` to our :term:`application +registry`, add a ``security.py`` module and give our :term:`root` +resource an :term:`ACL`. +Then we will add ``login`` and ``logout`` views, and modify the +existing views to make them return a ``logged_in`` flag to the +renderer and add :term:`permission` declarations to their ``view_config`` +decorators. -Configuring a ``pyramid`` Authentication Policy --------------------------------------------------- +Finally, we will add a ``login.pt`` template and change the existing +``view.pt`` and ``edit.pt`` to show a "Logout" link when not logged in. -For any :app:`Pyramid` application to perform authorization, we need to add a -``security.py`` module and we'll need to change our :term:`application -registry` to add an :term:`authentication policy` and a :term:`authorization -policy`. +The source code for this tutorial stage can be browsed via +`http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki/src/authorization/ +<http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki/src/authorization/>`_. Adding Authentication and Authorization Policies ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -64,6 +68,43 @@ user and groups sources. Note that the ``editor`` user is a member of the ``group:editors`` group in our dummy group data (the ``GROUPS`` data structure). +Giving Our Root Resource an ACL +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +We need to give our root resource object an :term:`ACL`. This ACL will be +sufficient to provide enough information to the :app:`Pyramid` security +machinery to challenge a user who doesn't have appropriate credentials when +he attempts to invoke the ``add_page`` or ``edit_page`` views. + +We need to perform some imports at module scope in our ``models.py`` file: + +.. code-block:: python + :linenos: + + from pyramid.security import Allow + from pyramid.security import Everyone + +Our root resource object is a ``Wiki`` instance. We'll add the following +line at class scope to our ``Wiki`` class: + +.. code-block:: python + :linenos: + + __acl__ = [ (Allow, Everyone, 'view'), + (Allow, 'group:editors', 'edit') ] + +It's only happenstance that we're assigning this ACL at class scope. An ACL +can be attached to an object *instance* too; this is how "row level security" +can be achieved in :app:`Pyramid` applications. We actually only need *one* +ACL for the entire system, however, because our security requirements are +simple, so this feature is not demonstrated. + +Our resulting ``models.py`` file will now look like so: + +.. literalinclude:: src/authorization/tutorial/models.py + :linenos: + :language: python + Adding Login and Logout Views ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -129,6 +170,38 @@ template. For example: logged_in = logged_in, edit_url = edit_url) +Adding ``permission`` Declarations to our ``view_config`` Decorators +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To protect each of our views with a particular permission, we need to pass a +``permission`` argument to each of our :class:`pyramid.view.view_config` +decorators. To do so, within ``views.py``: + +- We add ``permission='view'`` to the decorator attached to the + ``view_wiki`` and ``view_page`` view functions. This makes the + assertion that only users who possess the ``view`` permission + against the context resource at the time of the request may + invoke these views. We've granted + :data:`pyramid.security.Everyone` the view permission at the + root model via its ACL, so everyone will be able to invoke the + ``view_wiki`` and ``view_page`` views. + +- We add ``permission='edit'`` to the decorator attached to the + ``add_page`` and ``edit_page`` view functions. This makes the + assertion that only users who possess the effective ``edit`` + permission against the context resource at the time of the + request may invoke these views. We've granted the + ``group:editors`` principal the ``edit`` permission at the + root model via its ACL, so only a user whom is a member of + the group named ``group:editors`` will able to invoke the + ``add_page`` or ``edit_page`` views. We've likewise given + the ``editor`` user membership to this group via the + ``security.py`` file by mapping him to the ``group:editors`` + group in the ``GROUPS`` data structure (``GROUPS + = {'editor':['group:editors']}``); the ``groupfinder`` + function consults the ``GROUPS`` data structure. This means + that the ``editor`` user can add and edit pages. + Adding the ``login.pt`` Template ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -154,92 +227,29 @@ class="app-welcome align-right">`` div: <a href="${request.application_url}/logout">Logout</a> </span> -Giving Our Root Resource an ACL -------------------------------- - -We need to give our root resource object an :term:`ACL`. This ACL will be -sufficient to provide enough information to the :app:`Pyramid` security -machinery to challenge a user who doesn't have appropriate credentials when -he attempts to invoke the ``add_page`` or ``edit_page`` views. +Seeing Our Changes To ``views.py`` and our Templates +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -We need to perform some imports at module scope in our ``models.py`` file: +Our ``views.py`` module will look something like this when we're done: -.. code-block:: python +.. literalinclude:: src/authorization/tutorial/views.py :linenos: + :language: python - from pyramid.security import Allow - from pyramid.security import Everyone - -Our root resource object is a ``Wiki`` instance. We'll add the following -line at class scope to our ``Wiki`` class: +Our ``edit.pt`` template will look something like this when we're done: -.. code-block:: python +.. literalinclude:: src/authorization/tutorial/templates/edit.pt :linenos: + :language: xml - __acl__ = [ (Allow, Everyone, 'view'), - (Allow, 'group:editors', 'edit') ] - -It's only happenstance that we're assigning this ACL at class scope. An ACL -can be attached to an object *instance* too; this is how "row level security" -can be achieved in :app:`Pyramid` applications. We actually only need *one* -ACL for the entire system, however, because our security requirements are -simple, so this feature is not demonstrated. - -Our resulting ``models.py`` file will now look like so: +Our ``view.pt`` template will look something like this when we're done: -.. literalinclude:: src/authorization/tutorial/models.py +.. literalinclude:: src/authorization/tutorial/templates/view.pt :linenos: - :language: python - -Adding ``permission`` Declarations to our ``view_config`` Decorators --------------------------------------------------------------------- - -To protect each of our views with a particular permission, we need to pass a -``permission`` argument to each of our :class:`pyramid.view.view_config` -decorators. To do so, within ``views.py``: - -- We add ``permission='view'`` to the decorator attached to the ``view_wiki`` - view function. This makes the assertion that only users who possess the - ``view`` permission against the context resource at the time of the request - may invoke this view. We've granted :data:`pyramid.security.Everyone` the - view permission at the root model via its ACL, so everyone will be able to - invoke the ``view_wiki`` view. - -- We add ``permission='view'`` to the decorator attached to the ``view_page`` - view function. This makes the assertion that only users who possess the - effective ``view`` permission against the context resource at the time of - the request may invoke this view. We've granted - :data:`pyramid.security.Everyone` the view permission at the root model via - its ACL, so everyone will be able to invoke the ``view_page`` view. - -- We add ``permission='edit'`` to the decorator attached to the ``add_page`` - view function. This makes the assertion that only users who possess the - effective ``edit`` permission against the context resource at the time of - the request may invoke this view. We've granted the ``group:editors`` - principal the ``edit`` permission at the root model via its ACL, so only - the a user whom is a member of the group named ``group:editors`` will able - to invoke the ``add_page`` view. We've likewise given the ``editor`` user - membership to this group via thes ``security.py`` file by mapping him to - the ``group:editors`` group in the ``GROUPS`` data structure (``GROUPS = - {'editor':['group:editors']}``); the ``groupfinder`` function consults the - ``GROUPS`` data structure. This means that the ``editor`` user can add - pages. - -- We add ``permission='edit'`` to the decorator attached to the ``edit_page`` - view function. This makes the assertion that only users who possess the - effective ``edit`` permission against the context resource at the time of - the request may invoke this view. We've granted the ``group:editors`` - principal the ``edit`` permission at the root model via its ACL, so only - the a user whom is a member of the group named ``group:editors`` will able - to invoke the ``edit_page`` view. We've likewise given the ``editor`` user - membership to this group via thes ``security.py`` file by mapping him to - the ``group:editors`` group in the ``GROUPS`` data structure (``GROUPS = - {'editor':['group:editors']}``); the ``groupfinder`` function consults the - ``GROUPS`` data structure. This means that the ``editor`` user can edit - pages. + :language: xml Viewing the Application in a Browser ------------------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ We can finally examine our application in a browser. The views we'll try are as follows: @@ -267,35 +277,7 @@ as follows: credentials with the username ``editor``, password ``editor`` will show the edit page form being displayed. -Seeing Our Changes To ``views.py`` and our Templates ----------------------------------------------------- - -Our ``views.py`` module will look something like this when we're done: - -.. literalinclude:: src/authorization/tutorial/views.py - :linenos: - :language: python - -Our ``edit.pt`` template will look something like this when we're done: - -.. literalinclude:: src/authorization/tutorial/templates/edit.pt - :linenos: - :language: xml - -Our ``view.pt`` template will look something like this when we're done: - -.. literalinclude:: src/authorization/tutorial/templates/view.pt - :linenos: - :language: xml - -Revisiting the Application ---------------------------- - -When we revisit the application in a browser, and log in (as a result -of hitting an edit or add page and submitting the login form with the -``editor`` credentials), we'll see a Logout link in the upper right -hand corner. When we click it, we're logged out, and redirected back -to the front page. - - - +- After logging in (as a result of hitting an edit or add page and + submitting the login form with the ``editor`` credentials), we'll see + a Logout link in the upper right hand corner. When we click it, + we're logged out, and redirected back to the front page. diff --git a/docs/tutorials/wiki/definingmodels.rst b/docs/tutorials/wiki/definingmodels.rst index 3d2d01061..baf497458 100644 --- a/docs/tutorials/wiki/definingmodels.rst +++ b/docs/tutorials/wiki/definingmodels.rst @@ -89,70 +89,16 @@ something like this: :linenos: :language: python -Removing View Configuration ---------------------------- - -In a previous step in this chapter, we removed the -``tutorial.models.MyModel`` class. However, our ``views.py`` module still -attempts to import this class. Temporarily, we'll change ``views.py`` so -that it no longer references ``MyModel`` by removing its imports and removing -a reference to it from the arguments passed to the ``@view_config`` -:term:`configuration decoration` decorator which sits atop the ``my_view`` -view callable. - -The result of all of our edits to ``views.py`` will end up looking -something like this: - -.. literalinclude:: src/models/tutorial/views.py - :linenos: - :language: python - -Testing the Models ------------------- - -To make sure the code we just wrote works, we write tests for the model -classes and the appmaker. Changing ``tests.py``, we'll write a separate test -class for each model class, and we'll write a test class for the -``appmaker``. - -To do so, we'll retain the ``tutorial.tests.ViewTests`` class provided as a -result of the ``pyramid_zodb`` project generator. We'll add three test -classes: one for the ``Page`` model named ``PageModelTests``, one for the -``Wiki`` model named ``WikiModelTests``, and one for the appmaker named -``AppmakerTests``. - -When we're done changing ``tests.py``, it will look something like so: - -.. literalinclude:: src/models/tutorial/tests.py - :linenos: - :language: python +Viewing the Application in a Browser +------------------------------------ -Running the Tests ------------------ - -We can run these tests by using ``setup.py test`` in the same way we -did in :ref:`running_tests`. Assuming our shell's current working -directory is the "tutorial" distribution directory: - -On UNIX: - -.. code-block:: text - - $ ../bin/python setup.py test -q - -On Windows: - -.. code-block:: text - - c:\pyramidtut\tutorial> ..\Scripts\python setup.py test -q - -The expected output is something like this: +We can't. At this point, our system is in a "non-runnable" state; we'll need +to change view-related files in the next chapter to be able to start the +application successfully. If you try to start the application, you'll wind +up with a Python traceback on your console that ends with this exception: .. code-block:: text - ..... - ---------------------------------------------------------------------- - Ran 5 tests in 0.008s - - OK + ImportError: cannot import name MyModel +This will also happen if you attempt to run the tests. diff --git a/docs/tutorials/wiki/definingviews.rst b/docs/tutorials/wiki/definingviews.rst index 233e571f1..b6c083bbf 100644 --- a/docs/tutorials/wiki/definingviews.rst +++ b/docs/tutorials/wiki/definingviews.rst @@ -318,48 +318,3 @@ browser. The views we'll try are as follows: will generate an ``IndexError`` for the expression ``request.subpath[0]``. You'll see an interactive traceback facility provided by :term:`WebError`. - -Testing the Views -================= - -We'll modify our ``tests.py`` file, adding tests for each view function we -added above. As a result, we'll *delete* the ``ViewTests`` test in the file, -and add four other test classes: ``ViewWikiTests``, ``ViewPageTests``, -``AddPageTests``, and ``EditPageTests``. These test the ``view_wiki``, -``view_page``, ``add_page``, and ``edit_page`` views respectively. - -Once we're done with the ``tests.py`` module, it will look a lot like the -below: - -.. literalinclude:: src/views/tutorial/tests.py - :linenos: - :language: python - -Running the Tests -================= - -We can run these tests by using ``setup.py test`` in the same way we did in -:ref:`running_tests`. Assuming our shell's current working directory is the -"tutorial" distribution directory: - -On UNIX: - -.. code-block:: text - - $ ../bin/python setup.py test -q - -On Windows: - -.. code-block:: text - - c:\pyramidtut\tutorial> ..\Scripts\python setup.py test -q - -The expected result looks something like: - -.. code-block:: text - - ......... - ---------------------------------------------------------------------- - Ran 9 tests in 0.203s - - OK diff --git a/docs/tutorials/wiki/index.rst b/docs/tutorials/wiki/index.rst index 660bf3bd3..c984c4f01 100644 --- a/docs/tutorials/wiki/index.rst +++ b/docs/tutorials/wiki/index.rst @@ -23,5 +23,6 @@ tutorial can be browsed at definingmodels definingviews authorization + tests distributing diff --git a/docs/tutorials/wiki/src/tests/tutorial/tests.py b/docs/tutorials/wiki/src/tests/tutorial/tests.py new file mode 100644 index 000000000..d9ff866f1 --- /dev/null +++ b/docs/tutorials/wiki/src/tests/tutorial/tests.py @@ -0,0 +1,216 @@ +import unittest + +from pyramid import testing + +class PageModelTests(unittest.TestCase): + + def _getTargetClass(self): + from tutorial.models import Page + return Page + + def _makeOne(self, data=u'some data'): + return self._getTargetClass()(data=data) + + def test_constructor(self): + instance = self._makeOne() + self.assertEqual(instance.data, u'some data') + +class WikiModelTests(unittest.TestCase): + + def _getTargetClass(self): + from tutorial.models import Wiki + return Wiki + + def _makeOne(self): + return self._getTargetClass()() + + def test_it(self): + wiki = self._makeOne() + self.assertEqual(wiki.__parent__, None) + self.assertEqual(wiki.__name__, None) + +class AppmakerTests(unittest.TestCase): + def _callFUT(self, zodb_root): + from tutorial.models import appmaker + return appmaker(zodb_root) + + def test_it(self): + root = {} + self._callFUT(root) + self.assertEqual(root['app_root']['FrontPage'].data, + 'This is the front page') + +class ViewWikiTests(unittest.TestCase): + def test_it(self): + from tutorial.views import view_wiki + context = testing.DummyResource() + request = testing.DummyRequest() + response = view_wiki(context, request) + self.assertEqual(response.location, 'http://example.com/FrontPage') + +class ViewPageTests(unittest.TestCase): + def _callFUT(self, context, request): + from tutorial.views import view_page + return view_page(context, request) + + def test_it(self): + wiki = testing.DummyResource() + wiki['IDoExist'] = testing.DummyResource() + context = testing.DummyResource(data='Hello CruelWorld IDoExist') + context.__parent__ = wiki + context.__name__ = 'thepage' + request = testing.DummyRequest() + info = self._callFUT(context, request) + self.assertEqual(info['page'], context) + 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/thepage/edit_page') + + +class AddPageTests(unittest.TestCase): + def _callFUT(self, context, request): + from tutorial.views import add_page + return add_page(context, request) + + def test_it_notsubmitted(self): + from pyramid.url import resource_url + context = testing.DummyResource() + request = testing.DummyRequest() + request.subpath = ['AnotherPage'] + info = self._callFUT(context, request) + self.assertEqual(info['page'].data,'') + self.assertEqual( + info['save_url'], + resource_url(context, request, 'add_page', 'AnotherPage')) + + def test_it_submitted(self): + context = testing.DummyResource() + request = testing.DummyRequest({'form.submitted':True, + 'body':'Hello yo!'}) + request.subpath = ['AnotherPage'] + self._callFUT(context, request) + page = context['AnotherPage'] + self.assertEqual(page.data, 'Hello yo!') + self.assertEqual(page.__name__, 'AnotherPage') + self.assertEqual(page.__parent__, context) + +class EditPageTests(unittest.TestCase): + def _callFUT(self, context, request): + from tutorial.views import edit_page + return edit_page(context, request) + + def test_it_notsubmitted(self): + from pyramid.url import resource_url + context = testing.DummyResource() + request = testing.DummyRequest() + info = self._callFUT(context, request) + self.assertEqual(info['page'], context) + self.assertEqual(info['save_url'], + resource_url(context, request, 'edit_page')) + + def test_it_submitted(self): + context = testing.DummyResource() + request = testing.DummyRequest({'form.submitted':True, + 'body':'Hello yo!'}) + response = self._callFUT(context, request) + self.assertEqual(response.location, 'http://example.com/') + self.assertEqual(context.data, 'Hello yo!') + +class FunctionalTests(unittest.TestCase): + + viewer_login = '/login?login=viewer&password=viewer' \ + '&came_from=FrontPage&form.submitted=Login' + viewer_wrong_login = '/login?login=viewer&password=incorrect' \ + '&came_from=FrontPage&form.submitted=Login' + editor_login = '/login?login=editor&password=editor' \ + '&came_from=FrontPage&form.submitted=Login' + + def setUp(self): + import tempfile + import os.path + from tutorial import main + self.tmpdir = tempfile.mkdtemp() + + dbpath = os.path.join( self.tmpdir, 'test.db') + settings = { 'zodb_uri' : 'file://' + dbpath } + + app = main({}, **settings) + from repoze.zodbconn.middleware import EnvironmentDeleterMiddleware + app = EnvironmentDeleterMiddleware(app) + from webtest import TestApp + self.testapp = TestApp(app) + + def tearDown(self): + import shutil + shutil.rmtree( self.tmpdir ) + + def test_root(self): + res = self.testapp.get('/', status=302) + self.assertTrue(not res.body) + + def test_FrontPage(self): + res = self.testapp.get('/FrontPage', status=200) + self.assertTrue('FrontPage' in res.body) + + def test_unexisting_page(self): + res = self.testapp.get('/SomePage', status=404) + self.assertTrue('Not Found' in res.body) + + def test_successful_log_in(self): + res = self.testapp.get( self.viewer_login, status=302) + self.assertTrue(res.location == 'FrontPage') + + def test_failed_log_in(self): + res = self.testapp.get( self.viewer_wrong_login, status=200) + self.assertTrue('login' in res.body) + + def test_logout_link_present_when_logged_in(self): + res = self.testapp.get( self.viewer_login, status=302) + res = self.testapp.get('/FrontPage', status=200) + self.assertTrue('Logout' in res.body) + + def test_logout_link_not_present_after_logged_out(self): + res = self.testapp.get( self.viewer_login, status=302) + res = self.testapp.get('/FrontPage', status=200) + res = self.testapp.get('/logout', status=302) + self.assertTrue('Logout' not in res.body) + + def test_anonymous_user_cannot_edit(self): + res = self.testapp.get('/FrontPage/edit_page', status=200) + self.assertTrue('Login' in res.body) + + def test_anonymous_user_cannot_add(self): + res = self.testapp.get('/add_page/NewPage', status=200) + self.assertTrue('Login' in res.body) + + def test_viewer_user_cannot_edit(self): + res = self.testapp.get( self.viewer_login, status=302) + res = self.testapp.get('/FrontPage/edit_page', status=200) + self.assertTrue('Login' in res.body) + + def test_viewer_user_cannot_add(self): + res = self.testapp.get( self.viewer_login, status=302) + res = self.testapp.get('/add_page/NewPage', status=200) + self.assertTrue('Login' in res.body) + + def test_editors_member_user_can_edit(self): + res = self.testapp.get( self.editor_login, status=302) + res = self.testapp.get('/FrontPage/edit_page', status=200) + self.assertTrue('Editing' in res.body) + + def test_editors_member_user_can_add(self): + res = self.testapp.get( self.editor_login, status=302) + res = self.testapp.get('/add_page/NewPage', status=200) + self.assertTrue('Editing' in res.body) + + def test_editors_member_user_can_view(self): + res = self.testapp.get( self.editor_login, status=302) + res = self.testapp.get('/FrontPage', status=200) + self.assertTrue('FrontPage' in res.body) diff --git a/docs/tutorials/wiki/tests.rst b/docs/tutorials/wiki/tests.rst new file mode 100644 index 000000000..f3151dbcc --- /dev/null +++ b/docs/tutorials/wiki/tests.rst @@ -0,0 +1,78 @@ +============ +Adding Tests +============ + +We will now add tests for the models and the views and a few functional +tests in the ``tests.py``. Tests ensure that an application works, and +that it continues to work after some changes are made in the future. + +Testing the Models +================== + +We write tests for the model +classes and the appmaker. Changing ``tests.py``, we'll write a separate test +class for each model class, and we'll write a test class for the +``appmaker``. + +To do so, we'll retain the ``tutorial.tests.ViewTests`` class provided as a +result of the ``pyramid_zodb`` project generator. We'll add three test +classes: one for the ``Page`` model named ``PageModelTests``, one for the +``Wiki`` model named ``WikiModelTests``, and one for the appmaker named +``AppmakerTests``. + +Testing the Views +================= + +We'll modify our ``tests.py`` file, adding tests for each view function we +added above. As a result, we'll *delete* the ``ViewTests`` test in the file, +and add four other test classes: ``ViewWikiTests``, ``ViewPageTests``, +``AddPageTests``, and ``EditPageTests``. These test the ``view_wiki``, +``view_page``, ``add_page``, and ``edit_page`` views respectively. + + +Functional tests +================ + +We test the whole application, covering security aspects that are not +tested in the unit tests, like logging in, logging out, checking that +the ``viewer`` user cannot add or edit pages, but the ``editor`` user +can, and so on. + +Viewing the results of all our edits to ``tests.py`` +==================================================== + +Once we're done with the ``tests.py`` module, it will look a lot like the +below: + +.. literalinclude:: src/tests/tutorial/tests.py + :linenos: + :language: python + +Running the Tests +================= + +We can run these tests by using ``setup.py test`` in the same way we did in +:ref:`running_tests`. Assuming our shell's current working directory is the +"tutorial" distribution directory: + +On UNIX: + +.. code-block:: text + + $ ../bin/python setup.py test -q + +On Windows: + +.. code-block:: text + + c:\pyramidtut\tutorial> ..\Scripts\python setup.py test -q + +The expected result looks something like: + +.. code-block:: text + + ......... + ---------------------------------------------------------------------- + Ran 9 tests in 0.203s + + OK |
