Testing Applications with WebTest
+++++++++++++++++++++++++++++++++

:author: Ian Bicking <ianb@colorstudy.com>

.. toctree::
   :maxdepth: 3

   license
   modules/webtest

.. contents::

.. comment:

   >>> from dtopt import ELLIPSIS

Status & License
================

WebTest is an extraction of ``paste.fixture.TestApp``, rewriting
portions to use `WebOb <http://pythonpaste.org/webob/>`_.  It is under
active development as part of the Paste cloud of packages.

Feedback and discussion should take place on the `Paste list
<http://pythonpaste.org/community/>`_, and bugs should go into the
`Paste Trac instance <http://trac.pythonpaste.org>`_.

This library is licensed under an `MIT-style license <license.html>`_.

WebTest is in an svn repository at
`http://svn.pythonpaste.org/Paste/WebTest/trunk
<http://svn.pythonpaste.org/Paste/WebTest/trunk#egg=WebTest-dev>`_.  You
can check it out with::

    $ svn co http://svn.pythonpaste.org/Paste/WebTest/trunk WebTest

What This Does
==============

WebTest helps you test your WSGI-based web applications.  This can be
any application that has a WSGI interface, including an application
written in a framework that supports WSGI (which includes most
actively developed Python web frameworks -- almost anything that even
nominally supports WSGI should be testable).

With this you can test your web applications without starting an HTTP
server, and without poking into the web framework shortcutting
pieces of your application that need to be tested.  The tests WebTest
runs are entirely equivalent to how a WSGI HTTP server would call an
application.  By testing the full stack of your application, the
WebTest testing model is sometimes called a *functional test*,
*integration test*, or *acceptance test* (though the latter two are
not particularly good descriptions).  This is in contrast to a *unit
test* which tests a particular piece of functionality in your
application.  While complex programming tasks are often is suited to
unit tests, template logic and simple web programming is often best
done with functional tests; and regardless of the presence of unit
tests, no testing strategy is complete without high-level tests to
ensure the entire programming system works together.

WebTest helps you create tests by providing a convenient interface to
run WSGI applications and verify the output.

TestApp
=======

The most important object in WebTest is ``webtest.TestApp``, the
wrapper for WSGI applications.  To use it, you simply instantiate it
with your WSGI application.  (Note: if your WSGI application requires
any configuration, you must set that up manually in your tests.)

.. code-block:: python

    >>> from webtest import TestApp
    >>> from webob import Request, Response
    >>> from paste.urlmap import URLMap
    >>> map_app = URLMap()
    >>> form_html = map_app['/form.html'] = Response(content_type='text/html')
    >>> form_html.body = '''<html><body>
    ... <form action="/form-submit" method="POST">
    ...   <input type="text" name="name">
    ...   <input type="submit" name="submit" value="Submit!">
    ... </form></body></html>'''
    >>> app = TestApp(map_app)
    >>> res = app.get('/form.html')
    >>> res.status
    '200 OK'
    >>> res.form
    <webtest.Form object at ...>

Making Requests
---------------

To make a request, use:

.. code-block:: python

    app.get('/path', [headers], [extra_environ], ...)

This does a request for ``/path``, with any extra headers or WSGI
environment keys that you indicate.  This returns a response object,
based on `webob.Response <http://pythonpaste.org/webob/#response>`_.
It has some additional methods to make it easier to test.

If you want to do a POST request, use:

.. code-block:: python

    app.post('/path', {'vars': 'values'}, [headers], [extra_environ],
             [upload_files], ...)

Specifically the second argument is the *body* of the request.  You
can pass in a dictionary (or dictionary-like object), or a string
body (dictionary objects are turned into HTML form submissions).

You can also pass in the keyword argument upload_files, which is a
list of ``[(fieldname, filename, fild_content)]``.  File uploads use a
different form submission data type to pass the structured data.

For other verbs you can use:

    app.put(path, params, ...)
    app.delete(path, ...)

These do PUT and DELETE requests.


Modifying the Environment & Simulating Authentication
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The best way to simulate authentication is if your application looks
in ``environ['REMOTE_USER']`` to see if someone is authenticated.
Then you can simply set that value, like:

.. code-block:: python

    app.get('/secret', extra_environ=dict(REMOTE_USER='bob'))

If you want *all* your requests to have this key, do:

.. code-block:: python

    app = TestApp(my_app, extra_environ=dict(REMOTE_USER='bob'))

What Is Tested By Default
~~~~~~~~~~~~~~~~~~~~~~~~~

A key concept behind WebTest is that there's lots of things you
shouldn't have to check everytime you do a request.  It is assumed
that the response will either be a 2xx or 3xx response; if it isn't an
exception will be raised (you can override this for a request, of
course).  The WSGI application is tested for WSGI compliance with
a slightly modified version of `wsgiref.validate
<http://python.org/doc/current/lib/module-wsgiref.validate.html>`_
(modified to support arguments to ``InputWrapper.readline``)
automatically.  Also it checks that nothing is printed to the
``environ['wsgi.errors']`` error stream, which typically indicates a
problem (one that would be non-fatal in a production situation, but if
you are testing is something you should avoid).

To indicate another status is expected, use the keyword argument
``status=404`` to (for example) check that it is a 404 status, or
``status="*"`` to allow any status.

If you expect errors to be printed, use ``expect_errors=True``.

The Response Object
-------------------

The response object is based on `webob.Response
<http://pythonpaste.org/webob/#response>`_ with some additions to help
with testing.

The inherited attributes that are most interesting:

``response.status``:
    The text status of the response, e.g., ``"200 OK"``.

``response.headers``:
    A dictionary-like object of the headers in the response.

``response.body``:
    The text body of the response.

``response.request``:
    The `webob.Request object
    <http://pythonpaste.org/webob/#request>`_ used to generate this
    response.

The added methods:

``response.follow(**kw)``:
    Follows the redirect, returning the new response.  It is an error
    if this response wasn't a redirect.  Any keyword arguments are
    passed to ``app.get`` (e.g., ``status``).

``x in response``:
    Returns True if the string is found in the response body.
    Whitespace is normalized for this test.

``response.mustcontain(string1, string2, ...)``:
    Raises an error if any of the strings are not found in the
    response.  It also prints out the response in that case, so
    you can see the real response.

``response.showbrowser()``:
    Opens the HTML response in a browser; useful for debugging.

``str(response)``:
    Gives a slightly-compacted version of the response.  This is
    compacted to remove newlines, making it easier to use with
    `doctest <http://python.org/doc/current/lib/module-doctest.html>`_

``response.click(description=None, linkid=None, href=None, anchor=None, index=None, verbose=False)``: 
    Clicks the described link (`see docstring for more
    <./class-webtest.TestResponse.html#click>`_)

``response.forms``:
    Return a dictionary of forms; you can use both indexes (refer to
    the forms in order) or the string ids of forms (if you've given
    them ids) to identify the form.  See `Form Submissions <#form-submissions>`_ for
    more on the form objects.

``response.form``:
    If there is just a single form, this returns that.  It is an error
    if you use this and there are multiple forms.

Form Submissions
================

You can fill out and submit forms from your tests.  First you get the
form:

.. code-block:: python

    >>> res = app.get('/form.html')
    >>> form = res.form

Then you fill it in fields:

.. code-block:: python

    >>> form.action
    '/form-submit'
    >>> form.method
    'POST'
    >>> # dict of fields
    >>> fields = form.fields.items(); fields.sort(); fields
    [('name', [<webtest.Text object at ...>]), ('submit', [<webtest.Submit object at ...>])]
    >>> form['name'] = 'Bob'
    >>> # When names don't point to a single field:
    >>> form.set('name', 'Bob', index=0)

Then you can submit.  First we'll put up a simple test app to catch
the response:

.. code-block:: python

    >>> from webtest.debugapp import debug_app
    >>> map_app['/form-submit'] = debug_app

Then submit:

.. code-block:: python

    >>> # Submit with no particular submit button pressed:
    >>> res = form.submit()
    >>> # Or submit a button:
    >>> res = form.submit('submit')
    >>> print res
    Response: 200 OK
    Content-Type: text/plain
    ...
    -- Body ----------
    submit=Submit%21&name=Bob

Select fields can only be set to valid values (i.e., values in an
``<option>``) but you can also use
``form['select-field'].force_value('value')`` to enter values not
present in an option.

Parsing the Body
================

There are several ways to get parsed versions of the response.  These
are the attributes:

``response.html``:
    Return a `BeautifulSoup
    <http://www.crummy.com/software/BeautifulSoup/>`_ version of the
    response body.

``response.xml``:
    Return an `ElementTree
    <http://python.org/doc/current/lib/module-xml.etree.ElementTree.html>`_
    version of the response body.

``response.lxml``:
    Return an `lxml <http://codespeak.net/lxml/>`_ version of the
    response body.

``response.json``:
    Return the parsed JSON (parsed with `simplejson
    <http://svn.red-bean.com/bob/simplejson/tags/simplejson-1.7/docs/index.html>`_).

In each case the content-type must be correct or an AttributeError is
raised.  If you do not have the necessary library installed (none of
them are required by WebTest), you will get an ImportError.

Examples:

.. code-block:: python

    >>> from webtest import TestResponse
    >>> res = TestResponse(content_type='text/html', body='''
    ... <html><body><div id="content">hey!</div></body>''')
    >>> res.html
    <BLANKLINE>
    <html><body><div id="content">hey!</div></body></html>
    >>> res.html.__class__
    <class BeautifulSoup.BeautifulSoup at ...>
    >>> res.html.body.div.string
    u'hey!'
    >>> res.lxml
    <Element html at ...>
    >>> res.lxml.xpath('//body/div')[0].text
    'hey!'
    >>> res = TestResponse(content_type='application/json',
    ...                    body='{"a":1,"b":2}')
    >>> res.json
    {u'a': 1, u'b': 2}
    >>> res = TestResponse(content_type='application/xml',
    ...                    body='<xml><message>hey!</message></xml>')
    >>> res.xml
    <Element xml at ...>
    >>> res.xml[0].tag
    'message'
    >>> res.xml[0].text
    'hey!'
    >>> res.lxml
    <Element xml at ...>
    >>> res.lxml[0].tag
    'message'
    >>> res.lxml[0].text
    'hey!'


Framework Hooks
===============

Frameworks can detect that they are in a testing environment by the
presence (and truth) of the WSGI environmental variable
``"paste.testing"`` (the key name is inherited from
``paste.fixture``).

More generally, frameworks can detect that something (possibly a test
fixture) is ready to catch unexpected errors by the presence and truth
of ``"paste.throw_errors"`` (this is sometimes set outside of testing
fixtures too, when an error-handling middleware is in place).

Frameworks that want to expose the inner structure of the request may
use ``"paste.testing_variables"``.  This will be a dictionary -- any
values put into that dictionary will become attributes of the response
object.  So if you do ``env["paste.testing_variables"]['template'] =
template_name`` in your framework, then ``response.template`` will be
``template_name``.

News
====

1.2
---

* Fix form inputs; text inputs always default to the empty string, and
  unselected radio inputs default to nothing at all.  From Daniele
  Paolella.

* Fix following links with fragments (these fragments should not be
  sent to the WSGI application).  From desmaj.

* Added ``force_value`` to select fields, like
  ``res.form['select'].force_value("new_value")``.  This makes it
  possible to simulate forms that are dynamically updated.  From
  Matthew Desmarais.

* Fixed :method:`webtest.Response.mustcontain` when you pass in a
  ``no=[strings]`` argument.

1.1
---

* Changed the ``__str__`` of responses to make them more doctest
  friendly:

  - All headers are displayed capitalized, like Content-Type
  - Headers are sorted alphabetically

* Changed ``__repr__`` to only show the body length if the complete
  body is not shown (for short bodies the complete body is in the
  repr)

* Note: **these are backward incompatible changes** if you are using
  doctest (you'll have to update your doctests with the new format).

* Fixed exception in the ``.delete`` method.

* Added a ``content_type`` argument to ``app.post`` and ``app.put``,
  which sets the ``Content-Type`` of the request.  This is more
  convenient when testing REST APIs.

* Skip links in ``<script>...</script>`` tags (since that's not real
  markup).

1.0.2
-----

* Don't submit unnamed form fields.

* Checkboxes with no explicit ``value`` send ``on`` (previously they
  sent ``checked``, which isn't what browsers send).

* Support for ``<select multiple>`` fields (from Matthew Desmarais)

1.0.1
---

* Fix the ``TestApp`` validator's InputWrapper lacking support for
  readline with an argument as needed by the cgi module.

1.0
---

* Keep URLs in-tact in cases such as
  ``app.get('http://www.python.org')`` (so HTTP_HOST=www.python.org,
  etc).

* Fix ``lxml.html`` import, so lxml 2.0 users can get HTML lxml
  objects from ``resp.lxml``

* Treat ``<input type="image">`` like a submit button.

* Use ``BaseCookie`` instead of ``SimpleCookie`` for storing cookies
  (avoids quoting cookie values).

* Accept any ``params`` argument that has an ``items`` method (like
  MultiDict)

0.9
---

Initial release
