diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 52dbe626eea45d4d76b836c2ccb510c1d7bd81a8..730bd169937ef72c7d2321d6c657d338ce650f65 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -25,6 +25,7 @@ UnitTest:
when: on_success
script:
- python manage.py test tutorial_5.unit_tests
+ - python manage.py test tutorial_8.unit_tests
- python manage.py test -p "unit_test*.py"
DeployToHeroku:
diff --git a/README.md b/README.md
index 938c57a773af2d901d0cfffa89f263b6d03270b9..edbc52bd056ca232a38051a80307c42bdec7db21 100644
--- a/README.md
+++ b/README.md
@@ -11,6 +11,7 @@
5. Tutorial 5: Test Organization
6. Tutorial 6: Mutation Testing
7. Tutorial 7: Spiking & De-Spiking
+8. Tutorial 8: Using Mocks
**URL Heroku:** https://pmpl-izzan.herokuapp.com/
@@ -673,3 +674,52 @@ Berdasarkan buku **Test-Driven Development with Python 2nd Edition,** tutorial i
1. Mengimplementasikan *custom dedicated list* untuk setiap pengguna, dengan menerapkan **Passwordless Authentication**, dengan menyimpan *entity* `Token` di file `models.py` dari app yang berisikan email pengguna dan sebuah **unique id (uid)** yang di-*generate* secara otomatis oleh sistem. Pengimplementasian fitur baru ini dilakukan di *development branch* dan tidak disertai TDD, dikarenakan masih bersifat ***trial and error***. Hal ini dapat disebut juga dengan **Spiking**.
2. Setelah fitur *passwordless authentication* berhasil diimplementasikan pada *development branch*, dilakukan implementasi secara menyeluruh dengan melibatkan proses TDD didalamnya, dan dilakukan pada *staging/production branch*. Hal ini disebut juga dengan **De-Spiking**.
+## Penjelasan Tutorial 8
+
+Berdasarkan buku **Test-Driven Development with Python 2nd Edition, bab 19.1 - 19.5,** terdapat beberapa perbedaan antara **manual mocking** dengan menggunakan ***mock library*** bawaan dari Django. Perbedaan utamanya terdapat pada penggunaan *method* yang akan ditest.
+
+Pada manual mocking, digunakan sebuah *method* baru yang memiliki fungsionalitas sama dengan *method* yang akan ditest, tetapi menggunakan data *dummy*, seperti contoh dibawah ini.
+
+```python
+ def test_sends_mail_to_address_from_post(self):
+ self.send_mail_called = False
+
+ #fake method, with same functionality as tutorial 7's send_mail method
+ def fake_send_mail(subject, body, from_email, to_list):
+ self.send_mail_called = True
+ self.subject = subject
+ self.body = body
+ self.from_email = from_email
+ self.to_list = to_list
+
+ # replacing tutorial 7's send_mail method with the fake one
+ tutorial_7.views.send_mail = fake_send_mail
+
+ self.client.post(
+ '/tutorial-7/send_email',
+ data={
+ 'email': 'izzanfi@hotmail.com',
+ }
+ )
+```
+
+**Penjelasan:** pada potongan kode diatas, terdapat *inner method* baru yang bernama `fake_send_mail` yang merupakan *method* dengan fungsionalitas yang sama dengan *method* `send_mail` yang terdapat pada file `views.py`, namun dengan data-data *dummy*. Kemudian, dilakukan *assignment* *method* `send_mail` dengan *inner method* yang sebelumnya sudah dibuat, dan dilakukan pemanggilan POST request secara normal.
+
+Django telah menyediakan sebuah *library* untuk melakukan *automatic mocking*, yaitu bernama `unittest.mock.path`. *Library* ini bekerja dengan menempelkan sebuah *decorator* pada *test case method* yang akan ditest, seperti contoh dibawah ini.
+
+```python
+@patch('tutorial_7.views.send_mail')
+def test_send_mail_to_address_from_post(self, mock_send_mail):
+ self.client.post(
+ '/tutorial-7/send_email',
+ data={
+ 'email': 'izzanfi@hotmail.com'
+ }
+ )
+
+ self.assertEqual(mock_send_mail.called, True)
+ (subject, body, from_email, to_list), kwargs = mock_send_mail.call_args
+```
+
+**Penjelasan:** pada potongan kode diatas, terdapat *decorator* yang diletakkan diatas *test case method*, yaitu `@patch('tutorial_7.views.send_mail')`, dimana *decorator* tersebut menandakan bahwa *test case method* dibawahnya akan melakukan *mocking* terhadap *method* `send_mail` yang terdapat di file `views.py` milik modul `tutorial_7`. Selanjutnya, *method* yang sudah dilakukan *mocking* tersebut disimpan dalam sebuah parameter yang bernama `mock_send_mail`. Dan dilakukan pengecekan apakah *method* tersebut terpanggil melalui POST request, dan pencocokan argumen-argumen yang diterima oleh *method* yang sudah dilakukan *mocking* tersebut.
+
diff --git a/tutorial/settings.py b/tutorial/settings.py
index 05b9effa4ae9a63be05151890a5466592d382fb0..62e305420a16f7bf550f77228e8a7fedda6f9182 100644
--- a/tutorial/settings.py
+++ b/tutorial/settings.py
@@ -51,6 +51,7 @@ INSTALLED_APPS = [
'tutorial_4',
'tutorial_5',
'tutorial_7',
+ 'tutorial_8',
]
AUTH_USER_MODEL = 'tutorial_7.User'
diff --git a/tutorial_2/migrations/0001_initial.py b/tutorial_2/migrations/0001_initial.py
index bee2c84f9b65342e449ae393ac166d343bcde3aa..8d9b7f699f5bdbe70d6764730ece401b1c216ba6 100644
--- a/tutorial_2/migrations/0001_initial.py
+++ b/tutorial_2/migrations/0001_initial.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# Generated by Django 1.11.17 on 2019-11-14 10:36
+# Generated by Django 1.11.17 on 2019-11-27 14:19
from __future__ import unicode_literals
from django.db import migrations, models
diff --git a/tutorial_7/authentication.py b/tutorial_7/authentication.py
index ddac1062934588636f12a3f13c1d48414aedaac1..5e0a1ee30bd45dc1762759934d44822c1fcfc79e 100644
--- a/tutorial_7/authentication.py
+++ b/tutorial_7/authentication.py
@@ -1,24 +1,20 @@
import sys
-from .models import ListUser, Token
+from .models import User, Token
class PasswordlessAuthenticationBackend(object):
def authenticate(self, 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)
-
+ 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
def get_user(self, email):
- return ListUser.objects.get(email=email)
+ try:
+ return User.objects.get(email=email)
+ except User.DoesNotExist:
+ return None
diff --git a/tutorial_7/functional_tests/test_login.py b/tutorial_7/functional_tests/test_login.py
index b1c02e1a28420d50f49fa2b520fda055f7c5a224..8628682d07bc994195fd42ffdcb65f428fe17a68 100644
--- a/tutorial_7/functional_tests/test_login.py
+++ b/tutorial_7/functional_tests/test_login.py
@@ -18,7 +18,7 @@ class LoginTest(FunctionalTest):
# A message appears telling her an email has been sent
self.wait_for(lambda: self.assertIn(
- 'Verification Email Sent.',
+ "Check your email, you'll find a message with a link that will log you into the site.",
selenium_host.find_element_by_tag_name('body').text
))
diff --git a/tutorial_7/functional_tests/test_models.py b/tutorial_7/functional_tests/test_models.py
index 5f1242a99d94f8dad82bec6fa337ed80b2de3596..49870cdade972a578682e88e149d3f03df62ddcd 100644
--- a/tutorial_7/functional_tests/test_models.py
+++ b/tutorial_7/functional_tests/test_models.py
@@ -15,6 +15,7 @@ class UserModelTest(TestCase):
user = User(email='a@b.com')
self.assertEqual(user.pk, 'a@b.com')
+
class TokenModelTest(TestCase):
def test_links_user_with_auto_generated_uid(self):
diff --git a/tutorial_7/migrations/0001_initial.py b/tutorial_7/migrations/0001_initial.py
index 3df977c8b5ada80154b5d114e37bfb6f3f72266c..0871b72fa83366597438f7ebfa2bec4862b3e964 100644
--- a/tutorial_7/migrations/0001_initial.py
+++ b/tutorial_7/migrations/0001_initial.py
@@ -1,8 +1,9 @@
# -*- coding: utf-8 -*-
-# Generated by Django 1.11.17 on 2019-11-14 10:40
+# Generated by Django 1.11.17 on 2019-11-27 14:19
from __future__ import unicode_literals
from django.db import migrations, models
+import uuid
class Migration(migrations.Migration):
@@ -55,7 +56,7 @@ class Migration(migrations.Migration):
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('email', models.EmailField(max_length=254)),
- ('uid', models.CharField(max_length=255)),
+ ('uid', models.CharField(default=uuid.uuid4, max_length=255)),
],
),
]
diff --git a/tutorial_7/migrations/0002_auto_20191114_1836.py b/tutorial_7/migrations/0002_auto_20191114_1836.py
deleted file mode 100644
index f5ea26374777ec7c9310f4eb281b84001977238c..0000000000000000000000000000000000000000
--- a/tutorial_7/migrations/0002_auto_20191114_1836.py
+++ /dev/null
@@ -1,21 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.17 on 2019-11-14 11:36
-from __future__ import unicode_literals
-
-from django.db import migrations, models
-import uuid
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('tutorial_7', '0001_initial'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='token',
- name='uid',
- field=models.CharField(default=uuid.uuid4, max_length=255),
- ),
- ]
diff --git a/tutorial_7/migrations/0002_user_last_login.py b/tutorial_7/migrations/0002_user_last_login.py
new file mode 100644
index 0000000000000000000000000000000000000000..fd90b26ee612d7e06a66ea7a0fa2a72895edd71e
--- /dev/null
+++ b/tutorial_7/migrations/0002_user_last_login.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.17 on 2019-11-27 14:26
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('tutorial_7', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='user',
+ name='last_login',
+ field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
+ preserve_default=False,
+ ),
+ ]
diff --git a/tutorial_7/models.py b/tutorial_7/models.py
index 1f2495b48c72d45c105962d676dfdced74a0352c..cb1b316ef9a15ee664a2074c532a84e0e8c53821 100644
--- a/tutorial_7/models.py
+++ b/tutorial_7/models.py
@@ -27,6 +27,7 @@ class Token(models.Model):
class User(models.Model):
email = models.EmailField(primary_key=True)
+ last_login = models.DateTimeField(auto_now_add=True)
REQUIRED_FIELDS = []
USERNAME_FIELD = 'email'
is_anonymous = False
diff --git a/tutorial_7/templates/layout/base.html b/tutorial_7/templates/layout/base.html
index 7b6fbf09986447e4f3c5b245d2bc4653a17488ee..07135e62b6dc0b6d827b5babd51259c59ebac798 100644
--- a/tutorial_7/templates/layout/base.html
+++ b/tutorial_7/templates/layout/base.html
@@ -18,7 +18,7 @@
@import url(https://fonts.googleapis.com/css?family=Roboto|Roboto+Slab);
.container, .footer-down, .header-up{
font-family: "Roboto", "sans-serif";
- font-weight: normal;
+ font-weight: bold;
}
diff --git a/tutorial_7/templates/partials/header.html b/tutorial_7/templates/partials/header.html
index 7b28dea8d27685a838e8d8f81c386bed8e5bea3e..c0f6b01fb17c137433dec1972a2f69c9623de595 100644
--- a/tutorial_7/templates/partials/header.html
+++ b/tutorial_7/templates/partials/header.html
@@ -17,7 +17,7 @@
- Tutorial 7 PMPL
+ Tutorial 7 PMPL Hehe
diff --git a/tutorial_7/templates/tutorial_7.html b/tutorial_7/templates/tutorial_7.html
index a6fb02f1e0264506623ce4b1ec056a5ba125cb53..30675165db89c368945d4f19d6ede6f720152d75 100644
--- a/tutorial_7/templates/tutorial_7.html
+++ b/tutorial_7/templates/tutorial_7.html
@@ -22,11 +22,24 @@
{% else %}