This post covers how to write your first, very simple Python unit tests.
How the test code is organised
The first thing you need to decide is where your new tests should go. Fortunately Hypothesis tests are organised in a very simple way.
For every file in the the h directory
there’s a corresponding file, with the same filename except ending with _test.py
,
in the tests/h directory.
For example the tests for h/paginator.py
are in tests/h/paginator_test.py.
Similarly, every file in the src/memex directory
has a corresponding <filename>_test.py
file in
the tests/memex directory.
When you run a test command, such as tox -e py27-h tests/h
, pytest finds all
*_test.py
files in the tests/h
directory (and subdirectories).
For each test file pytest then finds all the top-level functions whose names
start with test_
and runs them. Pytest also finds all the classes whose names
start with Test
and for each class runs every method whose name starts with
test_
. Any other functions or class whose names don’t begin with test
are not run automatically by pytest, these are helper functions for the test
functions to call.
(For more about how pytest finds tests to run see the
complete documentation for pytest’s test discovery rules.)
In h we tend to put the code in a test in the same order as the corresponding source code in the module under test. The tests for the first function in the module would all go at the top of the test module, one after another, followed by the tests for the second function, and so on. Fixtures and other helpers go at the bottom of the test file.
Organising tests into classes
We often organise tests into classes in h, instead of just using top-level test functions. For example you might put all the tests for a module’s first function in one class, then all of the tests for the second function in a different test class, and so on. It’s easier to see where the tests for one function end and those for the next function begin if each function’s tests are indented under a class. Organising tests into classes also allows us to put helper functions, fixtures, patches etc (all of which we’ll see in later posts) in the classes that use them, which can reduce boilerplate and noise.
Exactly how we organise tests into classes varies. Sometimes it might be as
simple as putting all of the tests for one function into one test class. Other
times we’re writing tests for a class and put all of the tests for that class
in one test class, or separate the tests for each of the classes methods into
separate test classes for methods (this may depend on how big the class is and
how many different methods it has). Sometimes it’s useful to use separate test
classes to test the same code under different scenarios, for example
TestLoginControllerWhenLoggedIn
and TestLoginControllerWhenLoggedOut
,
because each test class can contain fixtures for that scenario (a test HTTP
request from a logged-in user, or an HTTP request from an unauthorized user)
that are applied to all tests in that class. Choose whichever approach works
best for your tests.
Writing tests
Let’s look at a simple as possible example test first.
h/accounts/util.py
contains a
validate_url() function
that validates the URLs that users enter for homepage links in their user
profiles. It’s the validation for the Link field in this user profile form:
validate_url()
raises a ValueError
exception if the string provided by the
user doesn’t look (vaguely) like a URL (this exception is caught by code
further up and turned into an error message that’s shown to the user).
If the URL does look valid then validate_url()
returns it, possibly with
http://
prepended to the front of the URL if it was missing.
def validate_url(url):
"""
Validate an HTTP(S) URL as a link for a user's profile.
Helper for use with Colander that validates a URL provided by a user as a
link for their profile.
Returns the normalized URL if successfully parsed or raises a ValueError
otherwise.
"""
...
The tests for this function are in
util_test.py.
Here’s a couple of very simple tests that test that validate_url()
returns
http://
URLs unmodified, and that it adds http://
to the start of URLs that
don’t have it:
def test_validate_url_returns_an_http_url_unmodified():
assert validate_url('http://github.com/jimsmith') == 'http://github.com/jimsmith'
def test_validate_url_adds_http_prefix_to_urls_that_lack_it():
assert validate_url('github.com/jimsmith') == 'http://github.com/jimsmith'
These are examples of the simplest possible test functions. They just call the function under test, passing in a certain argument to the function, and then use Python’s assert statement to test something about the function’s return value.
If the expression passed to the assert
statement evaluates to False
(that is, if validate_url()
doesn’t return the URL that the test expects),
then the assert
statement will raise an AssertionError
and the test will fail.
Otherwise the assert
does nothing, and if the test completes without an error
being raised it passes.
(For more about how the assert
statement works, see
Dan Bader’s Assert Statements in Python tutorial.)
When an assert fails pytest
outputs useful information about the failure
such as the values of the two sides of the expression (the result given by
validate_url()
and the result that the test had been expecting) and the
differences between them.