How we automated taking screenshots of Django applications
Both our documentation as well as our website contain a number of screenshots of our software. Taking these screenshots manually is really tedious, since you first need to populate your database with sensible test data, select a proper display resolution and then take screenshots separately for every language our software supports, so we can use them in the localized websites properly.
Once you’re finally done with it, your clock is ticking until the first major software change comes around that forces you to do it all over again. For example, we changed our application’s color scheme this May, added new languages starting in June and just last week reworked our sidebar navigation. Obviously, we’d be very interested in being able to re-create all screenshots easily and automatically.
We found a solution for this that we think of being very elegant, and we’d love to share it with you. We’ll need a few things in the process, so I’m going to explain them first.
Our ingredients
Selenium
If you haven’t heard of Selenium, the shortest way to describe it is as a remote control for browsers. It allows you to send high-level commands like “open this web page”, “click on this button” or “scroll down” to most major browsers. Selenium is usually used for integration and functional testing of frontend-heavy web applications, but comes in really handy here as well.
Chrom(e/ium) headless
Now that we’ve got a remote control for browsers, we also need a browser to control remotely. Just
using Firefox or any other browser works, but has some real disadvantages: Running a browser usually
requires a display server and a graphical environment, which is usually not available on servers. You
can fake it with tools with xvfb
, but it isn’t a lot of fun. Also, you don’t really want the size
of your screenshot to depend on the size of your display or the theme of your operating system that
controls how much of the screen is taken by the browser’s controls.
In the past, the way to go was a headless browser like PhantomJS, but PhantomJS has not only been deprecated this year, it also lacked a lot in terms of screenshot rendering, especially when it comes to font kerning.
Fortunately, Chrome gained the possibility to run headless without a display server last year, starting with Chrome 59. This way, we can make use of Chrome’s top-notch rendering while still being able to run this on servers efficiently.
py.test
py.test is a popular unit test runner for Python. Basically, it auto-discovers all test functions in your code and runs them in a flexible and configurable way. With py.test, tests are just simple Python functions, for example:
When running py.test
on that file, you will get an output report of the results that looks like this:
$ py.test
======= test session starts ========
collected 1 item
test_sample.py F
======= FAILURES ========
_______ test_answer ________
...
test_sample.py:5: AssertionError
======= 1 failed in 0.12 seconds ========
py.test also has a feature called fixtures, that allows you to define certain functions as pre-conditions for a test function. When a test function then defines an input parameter of the same name that a fixture has, py.test will automatically execute the fixture function and pass the return value as the input parameter to the test function:
pytest-django
The package pytest-django provides a set of fixtures and test markers that enable useful features such as resetting the database after every test and running a live HTTP server during tests, which will come in very handy.
The setup
py.test looks for a configuration file named pytest.ini
in its current drectory, which we can use to
change the terminology from tests to screenshots – that doesn’t make a lot of a difference, but it looks nicer:
[pytest]
DJANGO_SETTINGS_MODULE=pretix.testutils.settings
python_functions = shot_*
python_files = scene_*
python_classess = *Scene
addopts =--driver Chrome
Then, we use the conftest.py
file to create a number of useful fixtures. For example, we create fixtures
for things we want to have in our database when we do the screenshots:
But we also create fixtures for our general technical setup, such as a fixture that starts up a live HTTP server and a chrome browser and then already logs a user into the system:
Next, we need a fixture that provides our Chrome settings, such as the desired screen size:
Next, let’s create a parameterized fixture that will automatically be used for every test and runs the test with different active languages:
Finally, we create a short utility method that actually creates a screenshot:
Actually taking screenshots
With all of this setup, we can now define screenshots of specific pages in a very simple, declarative way where we just specify the objects we want to have in the database and the URL we want to visit:
Of course, we can also define more complex processes. For example, this is how we take screenshots of crating a new event within pretix, which is a multi-step process:
Taking a set of screenshots now is as easy as running a single command:
$ py.test scenes
…
Results (45.80s):
9 passed
If we change our design again, that’s all it takes to replace all screenshots in our design and documentation. If you want to see how it all works together in practice, of course our tool is available on GitHub for you to have a look at. Let us know what you think!