Skip to content

Commit 8de542b

Browse files
committed
loads more progress in 19
1 parent 684498b commit 8de542b

File tree

2 files changed

+99
-84
lines changed

2 files changed

+99
-84
lines changed

chapter_19_mocking.asciidoc

Lines changed: 98 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1161,10 +1161,11 @@ That gets us down to one failure:
11611161

11621162
[subs="specialcharacters,macros"]
11631163
----
1164-
ERROR: test_returns_new_user_with_correct_email_if_token_exists
1165-
(accounts.tests.test_authentication.AuthenticateTest)
1164+
ERROR: test_returns_new_user_with_correct_email_if_token_exists (accounts.tests
1165+
.test_authentication.AuthenticateTest.test_returns_new_user_with_correct_email_
1166+
if_token_exists)
11661167
[...]
1167-
accounts.models.DoesNotExist: User matching query does not exist.
1168+
accounts.models.User.DoesNotExist: User matching query does not exist.
11681169
11691170
FAILED (errors=1)
11701171
----
@@ -1177,7 +1178,7 @@ And we can handle the final case like this:
11771178
====
11781179
[source,python]
11791180
----
1180-
def authenticate(self, uid):
1181+
def authenticate(self, request, uid):
11811182
try:
11821183
token = Token.objects.get(uid=uid)
11831184
return User.objects.get(email=token.email)
@@ -1191,15 +1192,14 @@ And we can handle the final case like this:
11911192
That's turned out neater than our spike!
11921193

11931194

1194-
The get_user Method
1195-
^^^^^^^^^^^^^^^^^^^
1195+
==== The get_user Method
11961196

11971197

1198-
((("get_user method")))We've
1199-
handled the `authenticate` function which Django will use to log new
1200-
users in. The second part of the protocol we have to implement is the
1201-
`get_user` method, whose job is to retrieve a user based on their unique
1202-
identifier (the email address), or to return `None` if it can't find one
1198+
((("get_user method")))
1199+
We've handled the `authenticate` function which Django will use to log new users in.q
1200+
The second part of the protocol we have to implement is the `get_user` method,
1201+
whose job is to retrieve a user based on their unique identifier (the email address),
1202+
or to return `None` if it can't find one
12031203
(have another look at <<spike-reminder,the spiked code>> if you need a
12041204
reminder).
12051205

@@ -1213,21 +1213,16 @@ Here are a couple of tests for those two requirements:
12131213
[source,python]
12141214
----
12151215
class GetUserTest(TestCase):
1216-
12171216
def test_gets_user_by_email(self):
1218-
User.objects.create(email='[email protected]')
1219-
desired_user = User.objects.create(email='[email protected]')
1220-
found_user = PasswordlessAuthenticationBackend().get_user(
1221-
1222-
)
1217+
User.objects.create(email="[email protected]")
1218+
desired_user = User.objects.create(email="[email protected]")
1219+
found_user = PasswordlessAuthenticationBackend().get_user("[email protected]")
12231220
self.assertEqual(found_user, desired_user)
12241221
1225-
12261222
def test_returns_None_if_no_user_with_that_email(self):
12271223
self.assertIsNone(
1228-
PasswordlessAuthenticationBackend().get_user('[email protected]')
1224+
PasswordlessAuthenticationBackend().get_user("[email protected]")
12291225
)
1230-
12311226
----
12321227
====
12331228

@@ -1247,8 +1242,7 @@ Let's create a placeholder one then:
12471242
[source,python]
12481243
----
12491244
class PasswordlessAuthenticationBackend:
1250-
1251-
def authenticate(self, uid):
1245+
def authenticate(self, request, uid):
12521246
[...]
12531247
12541248
def get_user(self, email):
@@ -1259,9 +1253,10 @@ class PasswordlessAuthenticationBackend:
12591253
Now we get:
12601254

12611255

1256+
[subs="macros"]
12621257
----
12631258
self.assertEqual(found_user, desired_user)
1264-
AssertionError: None != <User: User object>
1259+
AssertionError: None != pass:specialcharacters[<User: User object ([email protected])>]
12651260
----
12661261

12671262
And (step by step, just to see if our test fails the way we think it will):
@@ -1278,9 +1273,11 @@ And (step by step, just to see if our test fails the way we think it will):
12781273

12791274
That gets us past the first assertion, and onto:
12801275

1276+
[subs="macros"]
12811277
----
12821278
self.assertEqual(found_user, desired_user)
1283-
AssertionError: <User: User object> != <User: User object>
1279+
AssertionError: pass:specialcharacters[<User: User object ([email protected])>] != pass:specialcharacters[<User: User object
1280+
12841281
----
12851282

12861283
And so we call `get` with the email as an argument:
@@ -1300,9 +1297,10 @@ And so we call `get` with the email as an argument:
13001297
Now our test for the `None` case fails:
13011298

13021299
----
1303-
ERROR: test_returns_None_if_no_user_with_that_email
1300+
ERROR: test_returns_None_if_no_user_with_that_email (accounts.tests.test_authen
1301+
tication.GetUserTest.test_returns_None_if_no_user_with_that_email)
13041302
[...]
1305-
accounts.models.DoesNotExist: User matching query does not exist.
1303+
accounts.models.User.DoesNotExist: User matching query does not exist.
13061304
----
13071305

13081306
Which prompts us to finish the method like this:
@@ -1321,9 +1319,9 @@ Which prompts us to finish the method like this:
13211319
----
13221320
====
13231321

1324-
<1> You could just use `pass` here, and the function would return `None`
1325-
by default. However, because we specifically need the function to return
1326-
`None`, the "explicit is better than implicit" rule applies here.
1322+
<1> You could just use `pass` here, and the function would return `None` by default.
1323+
However, because we specifically need the function to return `None`,
1324+
the "explicit is better than implicit" rule applies here.
13271325

13281326
That gets us to passing tests:
13291327

@@ -1336,8 +1334,7 @@ And we have a working authentication backend!
13361334

13371335

13381336

1339-
Using Our Auth Backend in the Login View
1340-
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
1337+
==== Using Our Auth Backend in the Login View
13411338

13421339
The final step is to use the backend in our login view. First we add it
13431340
to 'settings.py':
@@ -1348,9 +1345,9 @@ to 'settings.py':
13481345
====
13491346
[source,python]
13501347
----
1351-
AUTH_USER_MODEL = 'accounts.User'
1348+
AUTH_USER_MODEL = "accounts.User"
13521349
AUTHENTICATION_BACKENDS = [
1353-
'accounts.authentication.PasswordlessAuthenticationBackend',
1350+
"accounts.authentication.PasswordlessAuthenticationBackend",
13541351
]
13551352
13561353
[...]
@@ -1367,12 +1364,12 @@ back at the spike again:
13671364
[source,python]
13681365
----
13691366
def login(request):
1370-
print('login view', file=sys.stderr)
1371-
uid = request.GET.get('uid')
1367+
print("login view", file=sys.stderr)
1368+
uid = request.GET.get("uid")
13721369
user = auth.authenticate(uid=uid)
13731370
if user is not None:
13741371
auth.login(request, user)
1375-
return redirect('/')
1372+
return redirect("/")
13761373
----
13771374
====
13781375

@@ -1510,13 +1507,14 @@ What happens when we run the test? The first error is this:
15101507
$ pass:quotes[*python src/manage.py test accounts*]
15111508
[...]
15121509
AttributeError: <module 'accounts.views' from
1513-
'...goat-book/accounts/views.py'> does not have the attribute 'auth'
1510+
'...goat-book/src/accounts/views.py'> does not have the attribute 'auth'
15141511
----
15151512

1516-
TIP: `module foo does not have the attribute bar` is a common first failure
1517-
in a test that uses mocks. It's telling you that you're trying to mock
1518-
out something that doesn't yet exist (or isn't yet imported) in the target
1519-
module.
1513+
TIP: `module foo does not have the attribute bar`
1514+
is a common first failure in a test that uses mocks.
1515+
It's telling you that you're trying to mock out something
1516+
that doesn't yet exist (or isn't yet imported)
1517+
in the target module.
15201518

15211519
Once we import `django.contrib.auth`, the error changes:
15221520

@@ -1549,8 +1547,8 @@ function at all. Let's fix that, but get it deliberately wrong, just to see:
15491547
[source,python]
15501548
----
15511549
def login(request):
1552-
auth.authenticate('bang!')
1553-
return redirect('/')
1550+
auth.authenticate("bang!")
1551+
return redirect("/")
15541552
----
15551553
====
15561554

@@ -1575,8 +1573,8 @@ Let's give `authenticate` the arguments it expects then:
15751573
[source,python]
15761574
----
15771575
def login(request):
1578-
auth.authenticate(uid=request.GET.get('token'))
1579-
return redirect('/')
1576+
auth.authenticate(uid=request.GET.get("token"))
1577+
return redirect("/")
15801578
----
15811579
====
15821580

@@ -1648,12 +1646,11 @@ In any case, what do we get from running the test?
16481646
----
16491647
$ pass:quotes[*python src/manage.py test accounts*]
16501648
[...]
1651-
call(response.wsgi_request, mock_auth.authenticate.return_value)
16521649
AssertionError: None != call(<WSGIRequest: GET '/accounts/login?t[...]
16531650
----
16541651

1655-
Sure enough, it's telling us that we're not calling `auth.login` at all
1656-
yet. Let's try doing that. Deliberately wrong as usual first!
1652+
Sure enough, it's telling us that we're not calling `auth.login` at all yet.
1653+
Let's try doing that. Deliberately wrong as usual first!
16571654

16581655

16591656
[role="sourcecode"]
@@ -1662,9 +1659,9 @@ yet. Let's try doing that. Deliberately wrong as usual first!
16621659
[source,python]
16631660
----
16641661
def login(request):
1665-
auth.authenticate(uid=request.GET.get('token'))
1666-
auth.login('ack!')
1667-
return redirect('/')
1662+
auth.authenticate(uid=request.GET.get("token"))
1663+
auth.login("ack!")
1664+
return redirect("/")
16681665
----
16691666
====
16701667

@@ -1687,9 +1684,9 @@ Let's fix that:
16871684
[source,python]
16881685
----
16891686
def login(request):
1690-
user = auth.authenticate(uid=request.GET.get('token'))
1687+
user = auth.authenticate(uid=request.GET.get("token"))
16911688
auth.login(request, user)
1692-
return redirect('/')
1689+
return redirect("/")
16931690
----
16941691
====
16951692

@@ -1698,28 +1695,33 @@ Now we get this unexpected complaint:
16981695

16991696
[subs="specialcharacters,macros"]
17001697
----
1701-
ERROR: test_redirects_to_home_page (accounts.tests.test_views.LoginViewTest)
1698+
ERROR: test_redirects_to_home_page
1699+
(accounts.tests.test_views.LoginViewTest.test_redirects_to_home_page)
17021700
[...]
17031701
AttributeError: 'AnonymousUser' object has no attribute '_meta'
17041702
----
17051703

1706-
It's because we're still calling `auth.login` indiscriminately on any kind
1707-
of user, and that's causing problems back in our original test for the
1708-
redirect, which _isn't_ currently mocking out `auth.login`. We need to add an
1709-
`if` (and therefore another test), and while we're at it we'll learn about
1710-
patching at the class level.
1704+
It's because we're still calling `auth.login` indiscriminately on any kind of user,
1705+
and that's causing problems back in our original test for the redirect,
1706+
which _isn't_ currently mocking out `auth.login`.
1707+
We need to add an `if` (and therefore another test),
1708+
and while we're at it we'll learn about patching at the class level.
17111709

17121710

1713-
Patching at the Class Level
1714-
^^^^^^^^^^^^^^^^^^^^^^^^^^^
1711+
==== Patching at the Class Level
17151712

1716-
((("@patch")))((("mocks", "mock_auth variable")))((("patch decorator")))((("decorators", "patch decorator")))We
1717-
want to add another test, with another `@patch('accounts.views.auth')`,
1718-
and that's starting to get repetitive. We use the "three strikes" rule,
1719-
and we can move the patch decorator to the class level. This will have
1720-
the effect of mocking out `accounts.views.auth` in every single test
1721-
method in that class. That also means our original redirect test will
1722-
now also have the `mock_auth` variable injected:
1713+
((("@patch")))
1714+
((("mocks", "mock_auth variable")))
1715+
((("patch decorator")))
1716+
((("decorators", "patch decorator")))
1717+
We want to add another test, with another `@patch('accounts.views.auth')`,
1718+
and that's starting to get repetitive.
1719+
We use the "three strikes" rule,
1720+
and we can move the patch decorator to the class level.
1721+
This will have the effect of mocking out `accounts.views.auth`
1722+
in every single test method in that class.
1723+
That also means our original redirect test will now also
1724+
have the `mock_auth` variable injected:
17231725

17241726
//TODO: suggestion from TR discuss whether class-level mocks can be
17251727
// overridden in individual tests.
@@ -1779,27 +1781,34 @@ And we get it passing like this:
17791781
[source,python]
17801782
----
17811783
def login(request):
1782-
user = auth.authenticate(uid=request.GET.get('token'))
1784+
user = auth.authenticate(uid=request.GET.get("token"))
17831785
if user:
17841786
auth.login(request, user)
1785-
return redirect('/')
1787+
return redirect("/")
17861788
----
17871789
====
1788-
//45
17891790

17901791

17911792
// TODO: add a failure message? will help ppl with debugging login failures
17921793

1794+
The unit tests pass...
1795+
1796+
----
1797+
OK
1798+
----
17931799

1794-
So are we there yet?((("", startref="Mreduce19")))((("", startref="dupel19")))
1800+
1801+
So are we there yet?
1802+
((("", startref="Mreduce19")))((("", startref="dupel19")))
17951803

17961804

17971805
[role="pagebreak-before less_space"]
17981806
.Avoid Mock's Magic assert_called... Methods?
17991807
*******************************************************************************
18001808
If you've used `unittest.mock` before, you may have come across its special
18011809
`assert_called...`
1802-
http://bit.ly/2F9AEMY[methods], and you may be wondering why I didn't use them. For example, instead of doing:
1810+
http://bit.ly/2F9AEMY[methods], and you may be wondering why I didn't use them.
1811+
For example, instead of doing:
18031812
18041813
[role="skipme"]
18051814
[source,python]
@@ -1818,30 +1827,30 @@ a_mock.assert_called_with(foo, bar)
18181827
And the _mock_ library will raise an `AssertionError` for you if there is a
18191828
mismatch.
18201829
1821-
Why not use that? For me, the problem with these magic methods is that it's too
1822-
easy to make a silly typo and end up with a test that always passes:
1830+
Why not use that? For me, the problem with these magic methods is that
1831+
it's too easy to make a silly typo and end up with a test that always passes:
18231832
18241833
[role="skipme"]
18251834
[source,python]
18261835
----
18271836
a_mock.asssert_called_with(foo, bar) # will always pass
18281837
----
18291838
1830-
Unless you get the magic method name exactly right, then you will
1831-
just get a "normal" mock method, which just silently return another
1832-
mock, and you may not realise that you've written a test that tests
1833-
nothing at all.
1839+
Unless you get the magic method name exactly right,
1840+
then you will just get a "normal" mock method,
1841+
which just silently return another mock,
1842+
and you may not realise that you've written a test that tests nothing at all.
18341843
18351844
That's why I prefer to always have an explicit `unittest` method in there.
18361845
18371846
*******************************************************************************
18381847

18391848

1840-
The Moment of Truth: Will the FT Pass?
1841-
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1849+
=== The Moment of Truth: Will the FT Pass?
18421850

1843-
((("mocks", "functional test for")))((("functional tests (FTs)", "for mocks", secondary-sortas="mocks")))I
1844-
think we're just about ready to try our functional test!
1851+
((("mocks", "functional test for")))
1852+
((("functional tests (FTs)", "for mocks", secondary-sortas="mocks")))
1853+
I think we're just about ready to try our functional test!
18451854

18461855
Let's just make sure our base template shows a different nav bar for logged-in
18471856
and non–logged-in users (which our FT relies on):
@@ -1880,6 +1889,12 @@ and non–logged-in users (which our FT relies on):
18801889
----
18811890
====
18821891

1892+
1893+
****
1894+
TODO resume updates to chapter from here
1895+
****
1896+
1897+
18831898
And see if that...
18841899

18851900
[subs="specialcharacters,macros"]

0 commit comments

Comments
 (0)