@@ -1161,10 +1161,11 @@ That gets us down to one failure:
1161
1161
1162
1162
[subs="specialcharacters,macros"]
1163
1163
----
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)
1166
1167
[...]
1167
- accounts.models.DoesNotExist: User matching query does not exist.
1168
+ accounts.models.User. DoesNotExist: User matching query does not exist.
1168
1169
1169
1170
FAILED (errors=1)
1170
1171
----
@@ -1177,7 +1178,7 @@ And we can handle the final case like this:
1177
1178
====
1178
1179
[source,python]
1179
1180
----
1180
- def authenticate(self, uid):
1181
+ def authenticate(self, request, uid):
1181
1182
try:
1182
1183
token = Token.objects.get(uid=uid)
1183
1184
return User.objects.get(email=token.email)
@@ -1191,15 +1192,14 @@ And we can handle the final case like this:
1191
1192
That's turned out neater than our spike!
1192
1193
1193
1194
1194
- The get_user Method
1195
- ^^^^^^^^^^^^^^^^^^^
1195
+ ==== The get_user Method
1196
1196
1197
1197
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
1203
1203
(have another look at <<spike-reminder,the spiked code>> if you need a
1204
1204
reminder).
1205
1205
@@ -1213,21 +1213,16 @@ Here are a couple of tests for those two requirements:
1213
1213
[source,python]
1214
1214
----
1215
1215
class GetUserTest(TestCase):
1216
-
1217
1216
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] ")
1223
1220
self.assertEqual(found_user, desired_user)
1224
1221
1225
-
1226
1222
def test_returns_None_if_no_user_with_that_email(self):
1227
1223
self.assertIsNone(
1228
- PasswordlessAuthenticationBackend().get_user(' [email protected] ' )
1224
+ PasswordlessAuthenticationBackend().get_user(" [email protected] " )
1229
1225
)
1230
-
1231
1226
----
1232
1227
====
1233
1228
@@ -1247,8 +1242,7 @@ Let's create a placeholder one then:
1247
1242
[source,python]
1248
1243
----
1249
1244
class PasswordlessAuthenticationBackend:
1250
-
1251
- def authenticate(self, uid):
1245
+ def authenticate(self, request, uid):
1252
1246
[...]
1253
1247
1254
1248
def get_user(self, email):
@@ -1259,9 +1253,10 @@ class PasswordlessAuthenticationBackend:
1259
1253
Now we get:
1260
1254
1261
1255
1256
+ [subs="macros"]
1262
1257
----
1263
1258
self.assertEqual(found_user, desired_user)
1264
- AssertionError: None != <User: User object>
1259
+ AssertionError: None != pass:specialcharacters[ <User: User object ([email protected] )>]
1265
1260
----
1266
1261
1267
1262
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):
1278
1273
1279
1274
That gets us past the first assertion, and onto:
1280
1275
1276
+ [subs="macros"]
1281
1277
----
1282
1278
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
+
1284
1281
----
1285
1282
1286
1283
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:
1300
1297
Now our test for the `None` case fails:
1301
1298
1302
1299
----
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)
1304
1302
[...]
1305
- accounts.models.DoesNotExist: User matching query does not exist.
1303
+ accounts.models.User. DoesNotExist: User matching query does not exist.
1306
1304
----
1307
1305
1308
1306
Which prompts us to finish the method like this:
@@ -1321,9 +1319,9 @@ Which prompts us to finish the method like this:
1321
1319
----
1322
1320
====
1323
1321
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.
1327
1325
1328
1326
That gets us to passing tests:
1329
1327
@@ -1336,8 +1334,7 @@ And we have a working authentication backend!
1336
1334
1337
1335
1338
1336
1339
- Using Our Auth Backend in the Login View
1340
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
1337
+ ==== Using Our Auth Backend in the Login View
1341
1338
1342
1339
The final step is to use the backend in our login view. First we add it
1343
1340
to 'settings.py':
@@ -1348,9 +1345,9 @@ to 'settings.py':
1348
1345
====
1349
1346
[source,python]
1350
1347
----
1351
- AUTH_USER_MODEL = ' accounts.User'
1348
+ AUTH_USER_MODEL = " accounts.User"
1352
1349
AUTHENTICATION_BACKENDS = [
1353
- ' accounts.authentication.PasswordlessAuthenticationBackend' ,
1350
+ " accounts.authentication.PasswordlessAuthenticationBackend" ,
1354
1351
]
1355
1352
1356
1353
[...]
@@ -1367,12 +1364,12 @@ back at the spike again:
1367
1364
[source,python]
1368
1365
----
1369
1366
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" )
1372
1369
user = auth.authenticate(uid=uid)
1373
1370
if user is not None:
1374
1371
auth.login(request, user)
1375
- return redirect('/' )
1372
+ return redirect("/" )
1376
1373
----
1377
1374
====
1378
1375
@@ -1510,13 +1507,14 @@ What happens when we run the test? The first error is this:
1510
1507
$ pass:quotes[*python src/manage.py test accounts*]
1511
1508
[...]
1512
1509
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'
1514
1511
----
1515
1512
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.
1520
1518
1521
1519
Once we import `django.contrib.auth`, the error changes:
1522
1520
@@ -1549,8 +1547,8 @@ function at all. Let's fix that, but get it deliberately wrong, just to see:
1549
1547
[source,python]
1550
1548
----
1551
1549
def login(request):
1552
- auth.authenticate(' bang!' )
1553
- return redirect('/' )
1550
+ auth.authenticate(" bang!" )
1551
+ return redirect("/" )
1554
1552
----
1555
1553
====
1556
1554
@@ -1575,8 +1573,8 @@ Let's give `authenticate` the arguments it expects then:
1575
1573
[source,python]
1576
1574
----
1577
1575
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("/" )
1580
1578
----
1581
1579
====
1582
1580
@@ -1648,12 +1646,11 @@ In any case, what do we get from running the test?
1648
1646
----
1649
1647
$ pass:quotes[*python src/manage.py test accounts*]
1650
1648
[...]
1651
- call(response.wsgi_request, mock_auth.authenticate.return_value)
1652
1649
AssertionError: None != call(<WSGIRequest: GET '/accounts/login?t[...]
1653
1650
----
1654
1651
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!
1657
1654
1658
1655
1659
1656
[role="sourcecode"]
@@ -1662,9 +1659,9 @@ yet. Let's try doing that. Deliberately wrong as usual first!
1662
1659
[source,python]
1663
1660
----
1664
1661
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("/" )
1668
1665
----
1669
1666
====
1670
1667
@@ -1687,9 +1684,9 @@ Let's fix that:
1687
1684
[source,python]
1688
1685
----
1689
1686
def login(request):
1690
- user = auth.authenticate(uid=request.GET.get(' token' ))
1687
+ user = auth.authenticate(uid=request.GET.get(" token" ))
1691
1688
auth.login(request, user)
1692
- return redirect('/' )
1689
+ return redirect("/" )
1693
1690
----
1694
1691
====
1695
1692
@@ -1698,28 +1695,33 @@ Now we get this unexpected complaint:
1698
1695
1699
1696
[subs="specialcharacters,macros"]
1700
1697
----
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)
1702
1700
[...]
1703
1701
AttributeError: 'AnonymousUser' object has no attribute '_meta'
1704
1702
----
1705
1703
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.
1711
1709
1712
1710
1713
- Patching at the Class Level
1714
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^
1711
+ ==== Patching at the Class Level
1715
1712
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:
1723
1725
1724
1726
//TODO: suggestion from TR discuss whether class-level mocks can be
1725
1727
// overridden in individual tests.
@@ -1779,27 +1781,34 @@ And we get it passing like this:
1779
1781
[source,python]
1780
1782
----
1781
1783
def login(request):
1782
- user = auth.authenticate(uid=request.GET.get(' token' ))
1784
+ user = auth.authenticate(uid=request.GET.get(" token" ))
1783
1785
if user:
1784
1786
auth.login(request, user)
1785
- return redirect('/' )
1787
+ return redirect("/" )
1786
1788
----
1787
1789
====
1788
- //45
1789
1790
1790
1791
1791
1792
// TODO: add a failure message? will help ppl with debugging login failures
1792
1793
1794
+ The unit tests pass...
1795
+
1796
+ ----
1797
+ OK
1798
+ ----
1793
1799
1794
- So are we there yet?((("", startref="Mreduce19")))((("", startref="dupel19")))
1800
+
1801
+ So are we there yet?
1802
+ ((("", startref="Mreduce19")))((("", startref="dupel19")))
1795
1803
1796
1804
1797
1805
[role="pagebreak-before less_space"]
1798
1806
.Avoid Mock's Magic assert_called... Methods?
1799
1807
*******************************************************************************
1800
1808
If you've used `unittest.mock` before, you may have come across its special
1801
1809
`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:
1803
1812
1804
1813
[role="skipme"]
1805
1814
[source,python]
@@ -1818,30 +1827,30 @@ a_mock.assert_called_with(foo, bar)
1818
1827
And the _mock_ library will raise an `AssertionError` for you if there is a
1819
1828
mismatch.
1820
1829
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:
1823
1832
1824
1833
[role="skipme"]
1825
1834
[source,python]
1826
1835
----
1827
1836
a_mock.asssert_called_with(foo, bar) # will always pass
1828
1837
----
1829
1838
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.
1834
1843
1835
1844
That's why I prefer to always have an explicit `unittest` method in there.
1836
1845
1837
1846
*******************************************************************************
1838
1847
1839
1848
1840
- The Moment of Truth: Will the FT Pass?
1841
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1849
+ === The Moment of Truth: Will the FT Pass?
1842
1850
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!
1845
1854
1846
1855
Let's just make sure our base template shows a different nav bar for logged-in
1847
1856
and non–logged-in users (which our FT relies on):
@@ -1880,6 +1889,12 @@ and non–logged-in users (which our FT relies on):
1880
1889
----
1881
1890
====
1882
1891
1892
+
1893
+ ****
1894
+ TODO resume updates to chapter from here
1895
+ ****
1896
+
1897
+
1883
1898
And see if that...
1884
1899
1885
1900
[subs="specialcharacters,macros"]
0 commit comments