Django Views Testing with Scenario
First Blog Post Ever !
Feels weird. Not even sure how to markdown properly but here we are.
I wanted to share a small trick I use quite often when testing views.
Sometimes, your views heavily depends on the logged in user.
Let’s say our tests look like this right now:
from django.test import TestCase class SimpleTest(TestCase): @classmethod def setUpTestData(cls): # Set up data for the whole TestCase cls.username = 'johnny' cls.email = 'email@example.com' cls.password = 'password' cls.test_user = User.objects.create_user(cls.username, cls.email, cls.password) def test_details(self): self.client.login(username=self.username, password=self.password) response = self.client.get('/customer/details/') self.assertEqual(response.status_code, 200) def test_index(self): self.client.login(username=self.username, password=self.password) response = self.client.get('/customer/index/') self.assertEqual(response.status_code, 200)
It can quickly become cumbersome to handle multiple users over and over in your tests and it’s not quite clear what’s “johnny” able to do.
You also might need to force your user to log out, do something, log with another user etc…
What if you need to test the same bit of code with different users with different level of access?
Here’s my solution let’s use a context manager.
from contextlib import contextmanager from django.test import TestCase from .factories import UserFactory class ScenarioTestCase(TestCase): @contextmanager def log_user(self, user=None): if user is not None: self.client.login(username=user.username, password='password') try: yield finally: if user is not None: self.client.logout() @classmethod def setUpTestData(cls): cls.user1 = UserFactory.create() # Add here more users if needed def setUp(self): self.no_user_logged_in_scenario = cls.log_user(None) self.user1_logged_in_scenario = cls.log_user(self.user1) # Add here any other scenario you'd like
To make things a bit easier, I used a really simple
UserFactory using Factory Boy.
Here is the
factories.py file if you’re a bit lost:
import factory from django.conf import settings class UserFactory(factory.django.DjangoModelFactory): class Meta: model = settings.AUTH_USER_MODEL username = factory.Sequence(lambda n: "User %03d" % n) email = factory.LazyAttribute(lambda obj: '%firstname.lastname@example.org' % obj.username) @classmethod def _generate(cls, create, attrs): """Override the default _generate() to set the password.""" user = super()._generate(create, attrs) user.set_password('password') user.save() return user
Note that all my test users have a super safe password now.
We have our factory all set and a brand new
ScenarioTestCase in our test file ready to go. Let’s try it out !
class SimpleTest(ScenarioTestCase): def test_access(self): with self.user1_logged_in_scenario: self.assertEqual(self.client.get('/customer/details/').status_code, 200) with self.no_user_logged_in_scenario: self.assertEqual(self.client.get('/customer/details/').status_code, 403) # we can also use the contextmanager directly with self.log_user(self.user1): ...
That’s all folks! Cheers !
Update 1: Twidi advised me to use
setUpTestData instead of
setUp to handle user creation. This allows us to speed up the tests a bit,
setUpTestData is only called once for the whole test class. Read More..
Update 2: @ewjoachim pointed out in the comments that I was using a wrapper to create scenario (~ like a factory), which was quite pointless here, and that it will be far better with a
try/finally for the
yield to ensure the user is logged out in all case after the tests. Removing the wrapper around the contextmanager, we’re even able to call it directly now. I renamed
log_user, this should be more explicit.
A thousand thanks for your comments and improvements ! Also, this post may be better using pytest… But that will be for another post !