Ideally when making automated tests, you don’t have to mock anything. You just test exactly what would be executed in production. Some scenarios make that a challenge, though. What if you’re testing a view that relies on an external authentication service, like an LTI server?
It’s probably possible to automate an LTI server to run alongside my tests, just like a test database. But that adds a lot of complexity without adding much value. Although that would make the test environment more similar to the production one, right now I just want to be able to detect regressions in my custom view.
I’m making a test for a view that uses
LTIAuthMixin
from
the django-lti-provider
library. If you look at LTIAuthMixin, you can see that all the
authentication is handled in dispatch()
, a method that’s called with
each HTTP method of a view object (.get()
, .post()
, .put()
,
etc). Python has a
mock library
that I’ve never really gotten a handle on. I thought I could use this
library here, and tried to mock out LTIAuthMixin’s dispatch()
method. I was able to get my test to ignore LTI authentication, but I
couldn’t figure out how to tell my mocked dispatch method to still
return the necessary response data given a request with an authed
user. So I kept thinking of alternatives, keeping in mind that all I
cared about testing here is not even the view’s .get()
method, but
just .get_context_data()
.
Through a combination of Django’s RequestFactory and directly instantiating the view object, you can just alter its attributes as necessary. So I made a plan to not use Django’s test client, and just do something like this:
# LoggedInTestMixin just sets self.u to an authenticated user.
class MyLTILandingPageTest(LoggedInTestMixin, TestCase):
def setUp(self):
super(MyLTILandingPageTest, self).setUp()
self.factory = RequestFactory()
self.g = GraphFactory(title='Quiz graph',
needs_submit=True)
self.submission = SubmissionFactory(
graph=self.g, user=self.u, choice=3)
def test_get(self):
request = self.factory.get('/lti/landing/')
request.user = self.u
view = MyLTILandingPage()
ctx = view.get_context_data()
self.assertEqual(ctx.get('submissions').count(), 1)
The test above runs into a few
errors. self.lti
needs to be set,
and it also needs to respond to a course_context()
method call. This turned out to be sufficient:
class MockLTI(object):
def course_context(self, request):
return None
I also needed to attach the request object to the view I’ve instantiated, so it knows who the current user is. Here’s the completed test:
def test_get(self):
request = self.factory.get('/lti/landing/')
request.user = self.u
view = MyLTILandingPage()
view.lti = MockLTI()
view.request = request
ctx = view.get_context_data()
self.assertEqual(ctx.get('submissions').count(), 1)
submission = ctx.get('submissions').first()
self.assertEqual(submission.user, self.u)
self.assertEqual(submission.choice, 3)
Now my test confirms that this view’s context data
contains a graph submission connected to the authenticated
user. It doesn’t matter to get_context_data()
that
the user wasn’t actually authenticated with LTI.
So, if there’s a clearer, more idiomatic way to do this
using Python’s standard mock library, maybe a way to
override dispatch()
using mock, I’d like see some
examples.
Printed from: https://compiled.ctl.columbia.edu/articles/how-to-mock-non-standard-authentication-in-a-django-view-test/