Using Mocks to Test External Dependencies
In this chapter, we’ll start testing the parts of our code that send emails—i.e., the second item on our scratchpad:
In the functional test (FT), you saw that Django gives us a way of retrieving
any emails it sends by using the mail.outbox attribute.
But in this chapter, I want to demonstrate a widespread testing technique called mocking. So, for the purpose of these unit tests, we’ll pretend that this nice Django shortcut doesn’t exist.
Am I telling you not to use Django’s mail.outbox?
No—use it; it’s a neat helper.
But I want to teach you about mocks because they’re a useful general-purpose tool
for unit testing external dependencies.
You may not always be using Django!
And even if you are, you may not be sending email—any
interaction with a third-party API
is a place you might find yourself wanting to test with mocks.
Before We Start: Getting the Basic Plumbing In
Let’s just get a basic view and URL set up first. We can do so with a simple test to ensure that our new URL for sending the login email should eventually redirect back to the home page:
from django.test import TestCase
class SendLoginEmailViewTest(TestCase):
def test_redirects_to_home_page(self):
response = self.client.post(
"/accounts/send_login_email", data={"email": "[email protected]"}
)
self.assertRedirects(response, "/")
Wire up the include in superlists/urls.py,
plus the url in accounts/urls.py,
and get the test passing with something a bit like this:
from django.core.mail import send_mail (1)
from django.shortcuts import redirect
def send_login_email(request):
return redirect("/")
| 1 | I’ve added the import of the send_mail function as a placeholder for now. |
If you’ve got the plumbing right, the tests should pass at this point:
$ python src/manage.py test accounts [...] Ran 5 tests in 0.015s OK
OK, now we have a starting point—so let’s get mocking!
Mocking Manually—aka Monkeypatching
When we call send_mail in real life,
we expect Django to be making a connection to our email provider,
and sending an actual email across the public internet.
That’s not something we want to happen in our tests.
It’s a similar problem whenever you have code that has external side effects—calling
an API, sending out an SMS, integrating with a payment provider, whatever it may be.
When running our unit tests, we don’t want to be sending out real payments or making API calls across the internet. But we would still like a way of testing that our code is correct. Mocks[1] give us one way to do that.
Actually, one of the great things about Python is that its dynamic nature
makes it very easy to do things like mocking—or what’s sometimes called monkeypatching.
Let’s suppose that, as a first step,
we want to get to some code that invokes send_mail
with the right subject line, "from" address, and "to" address.
That would look something like this:
def send_login_email(request):
email = request.POST["email"]
# expected future code:
send_mail(
"Your login link for Superlists",
"some kind of body text tbc",
"noreply@superlists",
[email],
)
return redirect("/")
How can we test this without calling the real send_mail function?
The answer is that our test can ask Python to swap out the send_mail function
for a fake version, at runtime, just before we invoke the send_login_email view.
Check this out:
from django.test import TestCase
import accounts.views (2)
class SendLoginEmailViewTest(TestCase):
def test_redirects_to_home_page(self):
[...]
def test_sends_mail_to_address_from_post(self):
self.send_mail_called = False
def fake_send_mail(subject, body, from_email, to_list): (1)
self.send_mail_called = True
self.subject = subject
self.body = body
self.from_email = from_email
self.to_list = to_list
accounts.views.send_mail = fake_send_mail (2)
self.client.post(
"/accounts/send_login_email", data={"email": "[email protected]"}
)
self.assertTrue(self.send_mail_called)
self.assertEqual(self.subject, "Your login link for Superlists")
self.assertEqual(self.from_email, "noreply@superlists")
self.assertEqual(self.to_list, ["[email protected]"])
| 1 | We define a fake_send_mail function,
which looks like the real send_mail function,
but all it does is save some information about how it was called,
using some variables on self. |
| 2 | Then, before we execute the code under test by doing the self.client.post,
we swap out the real accounts.views.send_mail
with our fake version—it’s as simple as just assigning it. |
It’s important to realise that there isn’t really anything magical going on here; we’re just taking advantage of Python’s dynamic nature and scoping rules.
Up until we actually invoke a function, we can modify the variables it has access to,
as long as we get into the right namespace.
That’s why we import the top-level accounts module:
to be able to get down to the accounts.views module,
which is the scope in which the accounts.views.send_login_email function will run.
This isn’t even something that only works inside unit tests—you can do this kind of monkeypatching in any Python code! That may take a little time to sink in. See if you can convince yourself that it’s not all totally crazy—and then consider a couple of extra details that are worth knowing:
-
Why do we use
selfas a way of passing information around? It’s just a convenient variable that’s available both inside the scope of thefake_send_mailfunction and outside of it. We could use any mutable object, like a list or a dictionary, as long as we are making in-place changes to an existing variable that exists outside our fake function. (Feel free to have a play around with different ways of doing this, if you’re curious, and see what works and doesn’t.) -
The "before" is critical! I can’t tell you how many times I’ve sat there, wondering why a mock isn’t working, only to realise that I didn’t mock before I called the code under test.
Let’s see if our hand-rolled mock object will let us test-drive some code:
$ python src/manage.py test accounts
[...]
self.assertTrue(self.send_mail_called)
AssertionError: False is not true
So let’s call send_mail, naively:
from django.core.mail import send_mail (1)
[...]
def send_login_email(request):
send_mail() (2)
return redirect("/")
| 1 | This import should still be in the file from earlier, but in case an overenthusiastic IDE has removed it, I’m re-listing it for you here. |
| 2 | Here’s our new call to send_mail(). |
That gives:
TypeError: SendLoginEmailViewTest.test_sends_mail_to_address_from_post.<locals> .fake_send_mail() missing 4 required positional arguments: 'subject', 'body', 'from_email', and 'to_list'
It looks like our monkeypatch is working!
We’ve called send_mail, and it’s gone into our fake_send_mail function,
which wants more arguments.
Let’s try this:
def send_login_email(request):
send_mail("subject", "body", "from_email", ["to email"])
return redirect("/")
That gives:
self.assertEqual(self.subject, "Your login link for Superlists") AssertionError: 'subject' != 'Your login link for Superlists'
That’s working pretty well! Now we can work step-by-step, all the way through to something like this:
def send_login_email(request):
email = request.POST["email"]
send_mail(
"Your login link for Superlists",
"body text tbc",
"noreply@superlists",
[email],
)
return redirect("/")
And we have passing tests!
$ python src/manage.py test accounts Ran 6 tests in 0.016s OK
Brilliant! We’ve managed to write tests for some code, which would
ordinarily go out and try to send real emails across the internet,
and by "mocking out" the send_email function,
we’re able to write the tests and code all the same.[2]
But our hand-rolled mock has a couple of problems:
-
It involved a fair bit of boilerplate code, populating all those
self.xyzvariables to let us assert on them. -
More importantly, although we didn’t see this, the monkeypatching will persist from one test to the next, breaking isolation between tests. This can cause serious confusion.
The Python Mock Library
The mock package was added to the standard library as part of Python 3.3.
It provides a magical object called a Mock; try this out in a Python shell:
>>> from unittest.mock import Mock
>>> m = Mock()
>>> m.any_attribute
<Mock name='mock.any_attribute' id='140716305179152'>
>>> type(m.any_attribute)
<class 'unittest.mock.Mock'>
>>> m.any_method()
<Mock name='mock.any_method()' id='140716331211856'>
>>> m.foo()
<Mock name='mock.foo()' id='140716331251600'>
>>> m.called
False
>>> m.foo.called
True
>>> m.bar.return_value = 1
>>> m.bar(42, var='thing')
1
>>> m.bar.call_args
call(42, var='thing')
A mock is a magical object for a few reasons:
-
It responds to any request for an attribute or method call with other mocks.
-
You can configure it in turn to return specific values when called.
-
It enables you to inspect what it was called with.
Sounds like a useful thing to be able to use in our unit tests!
Using unittest.patch
And as if that weren’t enough,
the mock module also provides a helper function called patch,
which we can use to do the monkeypatching we did by hand earlier.
I’ll explain how it all works shortly, but let’s see it in action first:
from unittest import mock
from django.test import TestCase
[...]
class SendLoginEmailViewTest(TestCase):
def test_redirects_to_home_page(self):
[...]
@mock.patch("accounts.views.send_mail") (1)
def test_sends_mail_to_address_from_post(self, mock_send_mail): (2)
self.client.post(
"/accounts/send_login_email", data={"email": "[email protected]"}
)
self.assertEqual(mock_send_mail.called, True)
(subject, body, from_email, to_list), kwargs = mock_send_mail.call_args
self.assertEqual(subject, "Your login link for Superlists")
self.assertEqual(from_email, "noreply@superlists")
self.assertEqual(to_list, ["[email protected]"])
| 1 | Here’s the decorator—we’ll go into detail about how it works shortly. |
| 2 | Here’s the extra argument we add to the test method.
Again, detailed explanation to come,
but as you’ll see, it’s going to do most of the work that fake_send_mail
was doing before. |
If you rerun the tests, you’ll see they still pass. And because we’re always suspicious of any test that still passes after a big change, let’s deliberately break it just to see:
self.assertEqual(to_list, ["[email protected]"])
And let’s add a little debug print to our view as well,
to see the effects of the mock.patch:
def send_login_email(request):
email = request.POST["email"]
print(type(send_mail))
send_mail(
[...]
Let’s run the tests again:
$ python src/manage.py test accounts [...] ....<class 'function'> .<class 'unittest.mock.MagicMock'> [...] AssertionError: Lists differ: ['[email protected]'] != ['[email protected]'] [...] Ran 6 tests in 0.024s FAILED (failures=1)
Sure enough, the tests fail.
And we can see, just before the failure message,
that when we print the type of the send_mail function,
in the first unit test it’s a normal function,
but in the second unit test we’re seeing a mock object.
Let’s remove the deliberate mistake and dive into exactly what’s going on:
@mock.patch("accounts.views.send_mail") (1)
def test_sends_mail_to_address_from_post(self, mock_send_mail): (2)
self.client.post( (3)
"/accounts/send_login_email", data={"email": "[email protected]"}
)
self.assertEqual(mock_send_mail.called, True) (4)
(subject, body, from_email, to_list), kwargs = mock_send_mail.call_args (5)
self.assertEqual(subject, "Your login link for Superlists")
self.assertEqual(from_email, "noreply@superlists")
self.assertEqual(to_list, ["[email protected]"])
| 1 | The mock.patch() decorator takes a dot-notation name of an object to monkeypatch.
That’s the equivalent of manually replacing the send_mail in accounts.views.
The advantage of the decorator is that,
firstly, it automatically replaces the target with a mock.
And secondly, it automatically puts the original object back at the end!
(Otherwise, the object stays monkeypatched for the rest of the test run,
which might cause problems in other tests.) |
| 2 | patch then injects the mocked object into the test
as an argument to the test method.
We can choose whatever name we want for it,
but I usually use a convention of mock_ plus the original name of the object. |
| 3 | We call our view under test as usual,
but everything inside this test method has our mock applied to it,
so the view won’t call the real send_mail object;
it’ll be seeing mock_send_mail instead. |
| 4 | And we can now make assertions about what happened to that mock object during the test. We can see it was called… |
| 5 | …and we can also unpack its various positional and keyword call arguments,
to examine what it was called with.
(See [mock-call-args-sidebar] in the next chapter for a longer
explanation of .call_args.) |
All crystal clear? No? Don’t worry; we’ll do a couple more tests with mocks to see if they start to make more sense as we use them more.
Getting the FT a Little Further Along
First let’s get back to our FT and see where it’s failing:
$ python src/manage.py test functional_tests.test_login [...] AssertionError: 'Check your email' not found in 'Superlists\nEnter your email to log in\nStart a new To-Do list'
Submitting the email address currently has no effect. Hmmm. Currently our form is hardcoded to send to /accounts/send_login_email. Let’s switch to using the {% url %} syntax just to make sure it’s the right URL:
<form method="POST" action="{% url 'send_login_email' %}">
Does that help? Nope, same error. Why? Ah, nothing to do with the URL actually; it’s because we’re not displaying a success message after we send the user an email. Let’s add a test for that.
Testing the Django Messages Framework
We’ll use Django’s "messages framework", which is often used to display ephemeral "success" or "warning" messages to show the results of an action, something like what’s shown in A green success message.
Have a look at the Django messages docs if you haven’t come across it already. Testing Django messages is a bit contorted:
def test_adds_success_message(self):
response = self.client.post(
"/accounts/send_login_email",
data={"email": "[email protected]"},
follow=True, (1)
)
message = list(response.context["messages"])[0] (2)
self.assertEqual(
message.message,
"Check your email, we've sent you a link you can use to log in.",
)
self.assertEqual(message.tags, "success")
| 1 | We have to pass follow=True
to the test client to tell it to get the page after the 302-redirect. |
| 2 | Then we examine the response context for a messages iterable,
which we have to listify before it’ll play nicely.
(We’ll use these later in a template with {% for message in messages %}.) |
That gives:
$ python src/manage.py test accounts
[...]
message = list(response.context["messages"])[0]
IndexError: list index out of range
And we can get it passing with:
from django.contrib import messages
[...]
def send_login_email(request):
[...]
messages.success(
request,
"Check your email, we've sent you a link you can use to log in.",
)
return redirect("/")
Adding Messages to Our HTML
What happens next in the functional test? Ah. Still nothing. We need to actually add the messages to the page. Something like this:
[...]
</nav>
{% if messages %}
<div class="row">
<div class="col-md-12">
{% for message in messages %}
{% if message.level_tag == 'success' %}
<div class="alert alert-success">{{ message }}</div>
{% else %}
<div class="alert alert-warning">{{ message }}</div>
{% endif %}
{% endfor %}
</div>
</div>
{% endif %}
Now do we get a little further? Yes!
$ python src/manage.py test accounts [...] Ran 7 tests in 0.023s OK $ python src/manage.py test functional_tests.test_login [...] AssertionError: 'Use this link to log in' not found in 'body text tbc'
We need to fill out the body text of the email, with a link that the user can use to log in. Let’s just cheat for now though, by changing the value in the view:
send_mail(
"Your login link for Superlists",
"Use this link to log in",
"noreply@superlists",
[email],
)
That gets the FT a little further:
$ python src/manage.py test functional_tests.test_login [...] AssertionError: Could not find url in email body: Use this link to log in
OK, I think we can call the send_login_email view done for now:
Starting on the Login URL
We’re going to have to build some kind of URL! Let’s build the minimal thing, just a placeholder really:
class LoginViewTest(TestCase):
def test_redirects_to_home_page(self):
response = self.client.get("/accounts/login?token=abcd123")
self.assertRedirects(response, "/")
We’re imagining we’ll pass the token in as a GET parameter, after the ?.
It doesn’t need to do anything for now.
I’m sure you can find your way through to getting the boilerplate in for a basic URL and view, via errors like these:
-
No URL:
AssertionError: 404 != 302 : Response didn't redirect as expected: Response code was 404 (expected 302)
-
No view:
AttributeError: module 'accounts.views' has no attribute 'login'
-
Broken view:
ValueError: The view accounts.views.login didn't return an HttpResponse object. It returned None instead.
-
OK!
$ python src/manage.py test accounts [...] Ran 8 tests in 0.029s OK
And now we can give people a link to use. It still won’t do much though, because we still don’t have a token to give to the user.
Checking That We Send the User a Link with a Token
Back in our send_login_email view,
we’ve tested the email subject, and the "from", and "to" fields.
The body is the part that will have to include a token or URL they can use to log in.
Let’s spec out two tests for that:
from accounts.models import Token
[...]
class SendLoginEmailViewTest(TestCase):
def test_redirects_to_home_page(self):
[...]
def test_adds_success_message(self):
[...]
@mock.patch("accounts.views.send_mail")
def test_sends_mail_to_address_from_post(self, mock_send_mail):
[...]
def test_creates_token_associated_with_email(self): (1)
self.client.post(
"/accounts/send_login_email", data={"email": "[email protected]"}
)
token = Token.objects.get()
self.assertEqual(token.email, "[email protected]")
@mock.patch("accounts.views.send_mail")
def test_sends_link_to_login_using_token_uid(self, mock_send_mail): (2)
self.client.post(
"/accounts/send_login_email", data={"email": "[email protected]"}
)
token = Token.objects.get()
expected_url = f"http://testserver/accounts/login?token={token.uid}"
(subject, body, from_email, to_list), kwargs = mock_send_mail.call_args
self.assertIn(expected_url, body)
| 1 | The first test is fairly straightforward; it checks that the token we create in the database is associated with the email address from the POST request. |
| 2 | The second one is our second test using mocks.
We mock out the send_mail function again using the patch decorator,
but this time we’re interested in the body argument from the call arguments. |
Running them now will fail because we’re not creating any kind of token:
$ python src/manage.py test accounts [...] accounts.models.Token.DoesNotExist: Token matching query does not exist. [...] accounts.models.Token.DoesNotExist: Token matching query does not exist.
We can get the first one to pass by creating a token:
from accounts.models import Token
[...]
def send_login_email(request):
email = request.POST["email"]
token = Token.objects.create(email=email)
send_mail(
[...]
And now the second test prompts us to actually use the token in the body of our email:
[...] AssertionError: 'http://testserver/accounts/login?token=[...] not found in 'Use this link to log in' FAILED (failures=1)
So, we can insert the token into our email like this:
from django.urls import reverse
[...]
def send_login_email(request):
email = request.POST["email"]
token = Token.objects.create(email=email)
url = request.build_absolute_uri( (1)
reverse("login") + "?token=" + str(token.uid),
)
message_body = f"Use this link to log in:\n\n{url}"
send_mail(
"Your login link for Superlists",
message_body,
"noreply@superlists",
[email],
)
[...]
| 1 | request.build_absolute_uri deserves a mention—it’s
one way to build a "full" URL,
including the domain name and the HTTP(S) part, in Django.
There are other ways,
but they usually involve getting into the "sites" framework,
which gets complicated pretty quickly.
You can find lots more discussion on this if you’re curious
by doing a bit of googling. |
And the tests pass:
OK
I think that’s our send_login_email view done:
The next piece in the puzzle is the authentication backend, whose job it will be to examine tokens for validity and then return the corresponding users. Then, we need to get our login view to actually log users in, if they can authenticate.
De-spiking Our Custom Authentication Backend
Here’s how our authentication backend looked in the spike:
class PasswordlessAuthenticationBackend(BaseBackend):
def authenticate(self, request, uid):
print("uid", uid, file=sys.stderr)
if not Token.objects.filter(uid=uid).exists():
print("no token found", file=sys.stderr)
return None
token = Token.objects.get(uid=uid)
print("got token", file=sys.stderr)
try:
user = ListUser.objects.get(email=token.email)
print("got user", file=sys.stderr)
return user
except ListUser.DoesNotExist:
print("new user", file=sys.stderr)
return ListUser.objects.create(email=token.email)
def get_user(self, email):
return ListUser.objects.get(email=email)
Decoding this:
-
We take a UID and check if it exists in the database.
-
We return
Noneif it doesn’t. -
If it does exist, we extract an email address, and either find an existing user with that address or create a new one.
One if = One More Test
A rule of thumb for these sorts of tests:
any if means an extra test, and any try/except means an extra test. So, this should be about three tests.
How about something like this?
from django.http import HttpRequest
from django.test import TestCase
from accounts.authentication import PasswordlessAuthenticationBackend
from accounts.models import Token, User
class AuthenticateTest(TestCase):
def test_returns_None_if_no_such_token(self):
result = PasswordlessAuthenticationBackend().authenticate(
HttpRequest(), "no-such-token"
)
self.assertIsNone(result)
def test_returns_new_user_with_correct_email_if_token_exists(self):
email = "[email protected]"
token = Token.objects.create(email=email)
user = PasswordlessAuthenticationBackend().authenticate(
HttpRequest(), token.uid
)
new_user = User.objects.get(email=email)
self.assertEqual(user, new_user)
def test_returns_existing_user_with_correct_email_if_token_exists(self):
email = "[email protected]"
existing_user = User.objects.create(email=email)
token = Token.objects.create(email=email)
user = PasswordlessAuthenticationBackend().authenticate(
HttpRequest(), token.uid
)
self.assertEqual(user, existing_user)
In authenticate.py, we’ll just have a little placeholder:
class PasswordlessAuthenticationBackend:
def authenticate(self, request, uid):
pass
How do we get on?
$ python src/manage.py test accounts
.FE..........
======================================================================
ERROR: test_returns_new_user_with_correct_email_if_token_exists (accounts.tests
.test_authentication.AuthenticateTest.test_returns_new_user_with_correct_email_
if_token_exists)
---------------------------------------------------------------------
Traceback (most recent call last):
File "...goat-book/src/accounts/tests/test_authentication.py", line 21, in
test_returns_new_user_with_correct_email_if_token_exists
new_user = User.objects.get(email=email)
[...]
accounts.models.User.DoesNotExist: User matching query does not exist.
======================================================================
FAIL: test_returns_existing_user_with_correct_email_if_token_exists (accounts.t
ests.test_authentication.AuthenticateTest.test_returns_existing_user_with_corre
ct_email_if_token_exists)
---------------------------------------------------------------------
Traceback (most recent call last):
File "...goat-book/src/accounts/tests/test_authentication.py", line 31, in
test_returns_existing_user_with_correct_email_if_token_exists
self.assertEqual(user, existing_user)
~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^
AssertionError: None != <User: User object ([email protected])>
---------------------------------------------------------------------
Ran 13 tests in 0.038s
FAILED (failures=1, errors=1)
Here’s a first cut:
from accounts.models import Token, User
class PasswordlessAuthenticationBackend:
def authenticate(self, request, uid):
token = Token.objects.get(uid=uid)
return User.objects.get(email=token.email)
Now, instead of one FAIL and one ERROR,
we get two ERRORs:
$ python src/manage.py test accounts ERROR: test_returns_None_if_no_such_token (accounts.tests.test_authentication.A uthenticateTest.test_returns_None_if_no_such_token) [...] accounts.models.Token.DoesNotExist: Token matching query does not exist. ERROR: test_returns_new_user_with_correct_email_if_token_exists (accounts.tests .test_authentication.AuthenticateTest.test_returns_new_user_with_correct_email_ if_token_exists) [...] accounts.models.User.DoesNotExist: User matching query does not exist.
Notice that our third test,
test_returns_existing_user_with_correct_email_if_token_exists,
is actually passing. Our code does currently handle the "happy path",
where both the token and the user already exist in the database.
Let’s fix each of the remaining ones in turn.
Notice how the test names are telling us exactly what we need to do.
First, test_returns_None_if_no_such_token,
which is telling us what to do if the token doesn’t exist:
def authenticate(self, request, uid):
try:
token = Token.objects.get(uid=uid)
return User.objects.get(email=token.email)
except Token.DoesNotExist:
return None
That gets us down to one failure:
ERROR: test_returns_new_user_with_correct_email_if_token_exists (accounts.tests .test_authentication.AuthenticateTest.test_returns_new_user_with_correct_email_ if_token_exists) [...] accounts.models.User.DoesNotExist: User matching query does not exist. FAILED (errors=1)
OK, so we need to return a new_user_with_correct_email if_token_exists?
We can do that!
def authenticate(self, request, uid):
try:
token = Token.objects.get(uid=uid)
return User.objects.get(email=token.email)
except User.DoesNotExist:
return User.objects.create(email=token.email)
except Token.DoesNotExist:
return None
That’s turned out neater than our spike!
The get_user Method
We’ve handled the authenticate function, which Django will use to log new users in.
The second part of the protocol we have to implement is the get_user method,
whose job is to retrieve a user based on their unique identifier (the email address),
or to return None if it can’t find one.
(Have another look at the spiked code if you need a
reminder.)
Here are a couple of tests for those two requirements:
class GetUserTest(TestCase):
def test_gets_user_by_email(self):
User.objects.create(email="[email protected]")
desired_user = User.objects.create(email="[email protected]")
found_user = PasswordlessAuthenticationBackend().get_user("[email protected]")
self.assertEqual(found_user, desired_user)
def test_returns_None_if_no_user_with_that_email(self):
self.assertIsNone(
PasswordlessAuthenticationBackend().get_user("[email protected]")
)
And our first failure:
AttributeError: 'PasswordlessAuthenticationBackend' object has no attribute 'get_user'
Let’s create a placeholder one then:
class PasswordlessAuthenticationBackend:
def authenticate(self, request, uid):
[...]
def get_user(self, email):
pass
Now we get:
self.assertEqual(found_user, desired_user) AssertionError: None != <User: User object ([email protected])>
And (step by step, just to see if our test fails the way we think it will):
def get_user(self, email):
return User.objects.first()
That gets us past the first assertion, and onto:
self.assertEqual(found_user, desired_user) AssertionError: <User: User object ([email protected])> != <User: User object ([email protected])>
And so, we call get with the email as an argument:
def get_user(self, email):
return User.objects.get(email=email)
Now our test for the None case fails:
ERROR: test_returns_None_if_no_user_with_that_email (accounts.tests.test_authen tication.GetUserTest.test_returns_None_if_no_user_with_that_email) [...] accounts.models.User.DoesNotExist: User matching query does not exist.
That prompts us to finish the method like this:
def get_user(self, email):
try:
return User.objects.get(email=email)
except User.DoesNotExist:
return None (1)
| 1 | You could just use pass here, and the function would return None by default.
However, because we specifically need the function to return None,
the "explicit is better than implicit" rule applies here. |
That gets us to passing tests:
OK
And we have a working authentication backend!
Let’s call that a win and, in the next chapter, we’ll work on integrating it into our login view and getting our FT passing.
Comments