Writing tests interactively with a browser

Since your website is going to be used interactively, it can be helpful to write your tests in a similar way to how a user views the website, rather than doing it “blind”. This page outlines a method for doing that.

You can also view the content of this page as a video:

A complete example project for the setup can be found in the examples folder.

We are going to use pytest and pytest-django, and use all the Pytest integration tips, including the --show-browser option.

The essential technique is:

Using the example from the project, we’ve got a custom user class, and we’re going to test that the admin interface has been registered and still works.

We start with a test that looks like this:

from django_functest import FuncBaseMixin, ShortcutLoginMixin

from .base import SeleniumTestBase, WebTestBase
from .factories import create_user


class UserAdminBase(FuncBaseMixin, ShortcutLoginMixin):
    def test_change_details(self):
        pass


class UserAdminWT(UserAdminBase, WebTestBase):
    pass


class UserAdminSL(UserAdminBase, SeleniumTestBase):
    pass

The code here is just following the pattern from the guide, plus two things:

  • Adding the ShortcutLoginMixin

  • Importing a little custom utility function for creating users.

We now add our IPython prompt code into the base class method, so it looks like this:

def test_change_details(self):
     import IPython; IPython.embed()

Next, we are going to run the Selenium version of this test, with the browser window visible. Since I’ve only got one test so far, we can select it like this, using the selenium marker:

pytest accounts/tests/test_admin.py -m selenium --show-browser -s

Notice the use of -s to ensure that pytest doesn’t capture our input, which would break IPython.

This will run the test, and we’ll find ourselves at an IPython prompt with a browser window open.

We can then interactively write the test into IPython a line at a time, and watch our commands being executed. We’ll probably start something like this:

user = create_user(is_superuser=True)
self.shortcut_login(user)
self.get_url("admin:accounts_user_changelist")

We can then use “Inspect” and web browser dev tools to work out what elements to refer to and interact with. We might continue like this:

self.follow_link("#result_list tbody a")
self.fill({"#id_first_name": "Joe", "#id_last_name": "Bloggs"})
self.submit('input[value="Save"]')

We can also do our asserts:

user.refresh_from_db()
assert user.first_name == "Joe"

If we found that some of our setup code was actually wrong, or we missed something, there is no need to restart — just fix it by executing commands interactively and carry on.

Finally, we can copy back code from the terminal into our editor. You can use the history functionality in IPython (PageUp and PageDown) to look back through everything you typed. Use Ctrl-D to exit from IPython and allow pytest to finish.

In our editor, we’ll probably want to clean it up, and once done, we can run the tests again. This time, we’ll want to run both the WebTest and Selenium versions:

pytest accounts/tests/test_admin.py -v
============================= test session starts ==============================
platform linux -- Python 3.10.5, pytest-7.1.2, pluggy-1.0.0 -- /home/luke/.virtualenvs/django-functest-example/bin/python
cachedir: .pytest_cache
django: settings: example_project.settings (from ini)
rootdir: /home/luke/devel/django-functest/examples/example_project, configfile: pytest.ini
plugins: django-4.5.2, django-webtest-1.9.10
collecting ... collected 2 items

accounts/tests/test_admin.py::UserAdminWT::test_change_own_details  PASSED [ 50%]
accounts/tests/test_admin.py::UserAdminSL::test_change_own_details  PASSED [100%]

=============================== 2 passed in 1.87s ===============================

If we get any failures, we can also use the IPython prompt technique to debug easily, by insert the “embed” line just before the failing line of code.

Hopefully you’ll enjoy this method of writing tests! If you’ve got any more tips for improving this method, do let us know.