Adding Tests¶
How to add tests¶
Tin uses pytest
and pytest-django
to run tests. You can add tests
to a new app in one of two ways:
Creating a new file in the app called
tests.py
Creating a directory called
tests/
and adding files that are prefixed withtest_
inside the directory
You can then write a function whose name is prefixed test_
, and that
will be run as a test. A sample test might look like
def test_addition():
assert 1+1 == 2
You can then run the tests with python3 manage.py test
.
Writing good tests¶
Good tests should test the behavior of a specific view or method, and (ideally) not depend on anything else. For example, say we had this view
def my_view(request):
# this view is at /hi/
return redirect("/courses/")
A good test for this view would be
from tin.tests import is_redirect
def test_my_view(client):
response = client.get("/hi/")
assert is_redirect(response)
Utilities¶
Tin provides several fixtures to ease the process of writing tests.
The most commonly used is the login
decorator to login in a
django client
as a user. It also provides some utility functions
for checking the types of requests, like is_redirect
and is_login_redirect
.
Let’s look at an example using all three
from tin.tests import login, is_redirect, is_login_redirect
def test_redirect(client):
# client is an anonymous user
response = client.get(reverse("courses:add"))
assert is_login_redirect(response)
@login("student")
def test_redirect(client):
# client is logged in as a student
response = client.get(reverse("courses:add"))
assert is_redirect(response)
@login("admin")
def test_redirect(client):
# client is logged in as an admin
response = client.get(reverse("courses:add"))
assert not is_redirect(response)
Tips and Tricks¶
Pytest¶
You can pass arguments to pytest
by passing in --
after python3 manage.py test
. For example, to see the stdout
for all tests, run
python3 manage.py test -- -rP
Or to run specific sets of tests:
# run all tests in this file
python3 manage.py test -- tin/apps/courses/courses/tests.py
# run all tests in this directory
python3 manage.py test -- tin/apps/courses/assignments/tests/
# run test_redirect in courses/tests.py
python3 manage.py test -- tin/apps/courses/courses/tests.py::test_redirect
For more details, check out the pytest documentation.
Implementation Detail
As of the last time this page was updated,
python3 manage.py test
is a wrapper around the raw pytest
executable. As such, you can replace python3 manage.py test --
with just pytest
.
Note that this may not stay the same in the future, so if you encounter any problems
try using python3 manage.py test
as a first step.
Parameterization¶
Make heavy use of pytest.mark.parametrize
(docs).
Parameterizing tests allows for cleaner debugging when they fail. For example, consider this test case
def test_something():
for i in (1, 2):
assert 1+i == 2
When this test fails, it’s difficult to tell at which value of i
it failed at. Consider the following instead:
@pytest.mark.parametrize("i", (1, 2))
def test_something(i):
assert i+1 == 2
Now when this test fails, pytest tells us exactly which value of i
it failed at!
These can also be arbitrarily nested
@pytest.mark.parametrize("i", range(3))
@pytest.mark.parametrize("j", range(3))
def test_commutative_addition(i, j):
assert i+j == j+i
Here, test_commutative_addition
would be run
with values of \(i\) and \(j\) such that
\((i,j)\in\{(0, 0), (0, 1), (0, 2), (1, 0), (1, 1), ..., (2, 2)\}\)
Fixtures¶
Finding yourself repeating setup code a lot? Write a pytest fixture to do the setup for you.
For example, if you find yourself needing to create a second student often, you could do something like this
@pytest.fixture
def studentB(django_user_model):
user = django_user_model.objects.create(username="studentB")
user.is_student = True
user.save()
return user
def test_two_students(studentB):
# do stuff with studentB
...
Note
The django_user_model
fixture comes from the pytest-django
plugin.
Tin also provides some convenience fixtures, see Utilities for more details.
If a fixture only sets up something, and does not return
anything, use it with pytest.usefixtures
.
@pytest.fixture
def all_assigments_quiz(assignment):
assignment.is_quiz = True
assignment.save()
@pytest.usefixtures("all_assigments_quiz")
def test_something(assignment):
# test something, but now assignment is a quiz
...
Implementation Detail
This is actually how login
works, it’s simply a wrapper around
the admin_login
, student_login
and teacher_login
fixtures.