Test Mixins¶
ViewRequestFactoryTestMixin¶
In order to use the ViewRequestFactoryTestMixin
you need to import it and
add a few methods on your test case. A typical test case looks like this:
from django.test import TestCase
from django_libs.tests.mixins import ViewRequestFactoryTestMixin
from mixer.backend.django import mixer
from .. import views
class InvoiceDetailViewTestCase(ViewTestMixin, TestCase):
"""Tests for the ``InvoiceDetailView`` generic class based view."""
view_class = views.InvoiceDetailView
def setUp(self):
self.invoice = mixer.blend('invoices.Invoice')
self.user = self.invoice.user
def get_view_kwargs(self):
return {'pk': self.invoice.pk}
def test_view(self):
self.is_not_callable() # anonymous
self.is_callable(user=self.user)
self.is_postable(user=self.user, data={'amount': 1},
to_url_name='invoice_list')
self.is_postable(user=self.user, data={'amount': 1}, ajax=True)
Have a look at the docstrings in the code for further explanations: https://github.com/bitmazk/django-libs/blob/master/django_libs/tests/mixins.py
ViewTestMixin¶
In order to use the ViewTestMixin
you need to import it and implement
a few methods on your test case. A typical test case looks like this:
from django.test import TestCase
from django_libs.tests.mixins import ViewTestMixin
from your_invoice_app.tests.factories import InvoiceFactory
class InvoiceDetailViewTestCase(ViewTestMixin, TestCase):
"""Tests for the ``InvoiceDetailView`` generic class based view."""
def setUp(self):
self.invoice = InvoiceFactory()
self.user = self.invoice.user
def get_view_name(self):
return 'invoice_detail'
def get_view_kwargs(self):
return {'pk': self.invoice.pk}
def test_view(self):
self.should_redirect_to_login_when_anonymous()
self.should_be_callable_when_authenticated(self.user)
# your own tests here
For a slightly longer explanation on why the test looks like this, please read on...
Tutorial¶
It is a good idea to write a test that calls your view before you actually
write the view. And when you are at it, you might just as well test if a view
that is protected by login_required
, actually does require the user to
be logged in. Walking down that road, you might also just as well try to call
the view and manipulate the URL so that this user tries to access another
user’s objects. And so on, and so forth...
Fact is: You will be calling self.client.get
and self.client.post
a lot
in your integration tests (don’t confuse these tests with your unit tests).
Let’s assume that you have defined your urls.py
like this:
...
url(r'^invoice/(?P<pk>\d+)/', InvoiceDetailView.as_view(), name='invoice_detail'),
...
In order to test such a view, you would create an
integration_tests/views_tests.py
file and create a test case for this
view:
from django.test import TestCase
class InvoiceDetailViewTestCase(TestCase):
def test_view(self):
resp = self.client.get('/invoice/1/')
Writing the test this way is flawed because if you ever change that URL your test will fail. It would be much better to use the view name instead:
from django.core.urlresolvers import reverse
...
class InvoiceDetailViewTestCase(TestCase):
def test_view(self):
resp = self.client.get(reverse('invoice_detail'))
If your view is just slightly complex, you will have to call
self.client.get
several times and it is probably not a good idea to repeat
the string invoice_detail
over and over again, because that might change as
well. So let’s centralize the view name:
class InvoiceDetailViewTestCase(TestCase):
def get_view_name(self):
return 'invoice_detail'
def test_view(self):
resp = self.client.get(reverse(self.get_view_name()))
The code above was simplified. The reverse
calls would fail because the
view actually needs some kwargs. A proper call would look like this:
invoice = InvoiceFactory()
resp = self.client.get(reverse(self.get_view_name(), kwargs={
'pk': invoice.pk}))
This can get annoying when you need to call the view many times because most of the time you might call the view with the same kwargs. So let’s centralize the kwargs as well:
class InvoiceDetailViewTestCase(TestCase):
def setUp(self):
self.invoice = InvoiceFactory()
def get_view_name(self):
...
def get_view_kwargs(self):
return {'pk': self.invoice.pk}
def test_view(self):
resp = self.client.get(reverse(self.get_view_name(),
self.get_view_kwargs()))
This is much better. Someone who looks at your test, can easily identify the view name and the expected view kwargs that are needed to get a positive response from the view. When writing tests you don’t have to think about the view name or about constructing the view kwargs any more, which will speed up your workflow.
But this is still an awful lot of code to type. Which is why we created the ViewTestMixin:
class InvoiceDetailViewTestCase(ViewTestMixin, TestCase):
def setUp(self):
...
def get_view_name(self):
...
def get_view_kwargs(self):
...
def test_view(self):
resp = self.client.get(self.get_url())
Now we have got it down to a one-liner to call self.client.get
in a future
proof and maintainable way. After writing a few hundred tests with this
approach new patterns emerge. You will want to test almost all views if they
are accessible by anonymous or the opposite: If they are not accessible by
anonymous but by a logged in user.
For this reason the ViewTestMixin
provides a few convenience methods:
class InvoiceDetailViewTestCase(ViewTestMixin, TestCase):
...
def test_view(self):
user = UserFactory()
self.should_redirect_to_login_view_when_anonymous()
self.should_be_callable_when_authenticated(user)
If your view expectes some data payload (either POST or GET data), then you
can set self.data_payload
in your test. If all your tests need the same
data, you can override the get_data_payload()
method:
class InvoiceDetailViewTestCase(ViewTestMixin, TestCase):
...
def get_data_payload(self):
# If you stick to this implementation, you can still change the
# data payload for ``some`` of your tests.
if hasattr(self, 'data_payload'):
return self.data_payload
return {'foo': 'bar', }
def test_view(self):
user = UserFactory()
self.should_redirect_to_login_view_when_anonymous()
# Now your view will be called with the given data payload
self.should_be_callable_when_authenticated(user)
self.data_payload = {'foobar': 'barfoo'}
# Now you have changed the standard payload to be returned by
# ``get_data_payload``
self.should_be_callable_when_authenticated(user)
“is_callable” and “is_not_callable”¶
If a view becomes more complex, you might end up with rather many assertions for many different situations. If you take all these cases into account when testing, which you probably should, you will write a lot of:
def test_view(self):
# case 1
resp = self.client.get(self.get_url())
self.assertEqual(resp.status_code, 200, msg=(
'If this then that, because foo is bar.'))
# case 2
resp = ...
self.assertEqual(...)
# case 3
...
is_callable
and is_not_callable
let you quickly assign different values
to customize your actual assertion case in one method call.
is_callable
by default makes an assertion on status code 200.
is_not_callable
defaults to an assertion on status code 404.
Warning
Note if you used previous versions, that is_callable
will only
default to 200 in the future.
It’s best to use and_redirects_to
for a redirect assertion or if you
only want to make sure to get the right code set status_code
to 302.
Also the code
parameter changed into status_code
.
They can still be used, but you will get annoying warnings. So, you might as well change it right away.
Argument | Definition |
---|---|
method |
String that defines if either ‘post’ or ‘get’ is used. |
data |
dictionary with GET data payload or POST data. If not
provided it calls self.get_data_payload() instead. |
kwargs |
dictionary to overwrite view kwargs. If not provided, it
calls self.get_view_kwargs() instead. |
user |
Assign a user instance to log this user in first. As in
self.should_be_callable_when_authenticated() the
password is expected to be ‘test123’. |
anonymous |
If this is assigned True , the user is logged out
before the assertion. So basically you test with an
anonymous user. Default is False . |
and_redirects_to |
If set, it performs an assertRedirects assertion.
Note that, of course this will overwrite the
status_code to 302. |
status_code |
If set, it overrides the status code, the assertion is made with. |
ajax |
If True it will automatically set
HTTP_X_REQUESTED_WITH='XMLHttpRequest' to simulate
an ajax call. Defaults to False . |
You can also define no arguments to test according to your current situation. Then still, it is a handy shortcut.
Further methods are:
- should_be_callable_when_anonymous
- should_be_callable_when_has_correct_permissions
Have a look at the docstrings in the code for further explanations: https://github.com/bitmazk/django-libs/blob/master/django_libs/tests/mixins.py