Fakultas Ilmu Komputer UI

Commit 59021c39 authored by RANI LASMA ULI's avatar RANI LASMA ULI
Browse files

Merge branch 'testinggoat/ch19' into 'master'

Testinggoat/ch19

See merge request !10
parents a1266cfe ef2ac92a
Pipeline #25828 passed with stages
in 10 minutes and 35 seconds
......@@ -31,7 +31,7 @@ Deployment:
url: $HEROKU_APP_HOST
FunctionalTest:
image: python:3.6
image: python:3.7
stage: functionalTest
before_script:
- wget -qO- https://cli-assets.heroku.com/install-ubuntu.sh | sh
......@@ -48,8 +48,10 @@ FunctionalTest:
- unzip chromedriver_linux64.zip
- pip install -r requirements.txt
when: on_success
allow_failure: true
script:
- python manage.py test functional_tests
- python3 manage.py test functional_tests
after_script:
- heroku pg:reset DATABASE --app $HEROKU_NAME --confirm $HEROKU_NAME || true
- heroku run --app $HEROKU_NAME migrate || true
......@@ -8,6 +8,7 @@
- [Refactoring and Clean Code](#refactoring-and-clean-code)
- [Why Need Test Organization](#why-need-test-organization)
- [Mutation Testing](#mutation-testing)
- [Mocking Manually vs Library](#mocking-manually-vs-library)
- [URL](#url)
- [Install Modules](#install-modules)
......@@ -216,4 +217,96 @@ Untuk membunuh mutant ini, saya menambah test seperti berikut ini.
self.assertFalse('location' in response)
```
Hal ini akan membuat mutant menghasilkan kode response 302, sedangkan kode yang sebenarnya akan mengembalikan kode response 200.
\ No newline at end of file
Hal ini akan membuat mutant menghasilkan kode response 302, sedangkan kode yang sebenarnya akan mengembalikan kode response 200.
#### Mocking Manually vs Library
Mock adalah sebuah objek yang meniru tingkah laku dari sebuah objek.
Dengan adanya Mock, developer dipermudah untuk fokus menguji method suatu method tanpa harus memikirkan method lain (Unit Test). Kegiatan mocking object dapat dilakukan dengan 2 cara yaitu, secara manual dan menggunakan bantuan library yang telah disediakan bahasa tertentu, dalam hal ini Python. Berikut adalah perbedaan antara mocking secara manual dengan menggunakan library.
1. Mocking Manually
Dalam chapter 19, saya telah membuat fungsi `fake_send_email` untuk mengimitasi fungsi `send_login_email`. Fungsi `send_login_email` melibatkan fungsi `send_mail` yang merupakan bawaan dari module django.core.mail. Secara sederhana, `send_login_email` melibatkan modul lain dan tidak memungkinkan untuk menguji `send_login_email` tanpa fungsi tersebut. Oleh karena itu, saya membuat fungsi `fake_send_email` dan mengalihkan pemanggilan kode yang pada awalnya memanggil `send_login_email` menjadi `fake_send_email`.
```python
def send_login_email(request):
email = request.POST['email']
token = Token.objects.create(email=email)
url = request.build_absolute_uri(f'/accounts/login?token={token.uid}')
# imported from django.core.mail
# need to be mocked
send_mail(
'Your login link for Superlists', # subject
f'Use this link to log in:\n\n{url}', # body
'noreply@superlists', # from_email
[email],
)
messages.success(
request,
"Check your email, we've sent you a link you can use to log in.")
return redirect('/')
```
accounts/views.py
```python
def test_sends_mail_to_address_from_post(self):
self.send_mail_called = False
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
# send_mail() is replaced by fake_send_mail()
# HOW??
# account.views.send_mail means import method send_mail is replaced by fake_send_mail
# that's why import module should be put in the first line
accounts.views.send_mail = fake_send_mail
self.client.post('/accounts/send_login_email', data={
'email': 'edith@example.com'
})
self.assertTrue(mock_send_mail.called)
...
```
Finally, `fake_send_mail` successfully imitate `send_mail`. As you can see, `fake_send_mail` store all `send_mail` attributes in its attribute using `self`.
> Method has been mocked! So, are we done? Does it provide the best way to mock?
> How if other class need the exactly same mocking object?
> Now let's take a look to `Mocking using Library` section.
2. Mocking using Library
Now we do mocking the same method, `send_email`, using `unittest.mock` library.
```python
@patch('accounts.views.send_mail')
def test_sends_mail_to_address_from_post(self, mock_send_mail):
self.client.post('/accounts/send_login_email',
data={'email': 'edith@example.com'})
# get all arguments those involved in mocking
# arguments value retrieve in tuples and sorted as send_email
(subject, body, from_email, to_list), kwargs = mock_send_mail.call_args
...
```
You can see how much lines we reduce.
Moreover, import modules in the middle of code will screwing up the code quality (not best practice).
Also, using library for mocking is pretty simple - in this case just using @patch(`modules_path`) and we're done - and make the mocking object become reusable.
> **Summary**
> Manual Mock:
> 1. Dirty Code
> 2. Redundant with the real function, we should re-construct real object in the fake method to do
> imitation.
>
> Mocking with Library:
> 1. Readable code, no object creation in testing class
> 2. Fully-package! No need to write the construct the object (fake) to mock real object
from django.contrib import admin
# Register your models here.
from django.apps import AppConfig
class AccountsConfig(AppConfig):
name = 'accounts'
import sys
from accounts.models import ListUser, 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)
def get_user(self, email):
return ListUser.objects.get(email=email)
\ No newline at end of file
# -*- coding: utf-8 -*-
# Generated by Django 1.11.24 on 2019-11-21 10:14
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0008_alter_user_username_max_length'),
]
operations = [
migrations.CreateModel(
name='ListUser',
fields=[
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('email', models.EmailField(max_length=254, primary_key=True, serialize=False)),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Token',
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)),
],
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11.24 on 2019-11-21 16:11
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='User',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('email', models.EmailField(max_length=254)),
],
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11.24 on 2019-11-21 16:11
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0002_user'),
]
operations = [
migrations.AlterField(
model_name='user',
name='email',
field=models.EmailField(max_length=254, unique=True),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11.24 on 2019-11-21 16:13
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0003_auto_20191121_2311'),
]
operations = [
migrations.RemoveField(
model_name='user',
name='id',
),
migrations.AlterField(
model_name='user',
name='email',
field=models.EmailField(max_length=254, primary_key=True, serialize=False),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11.24 on 2019-11-22 10:26
from __future__ import unicode_literals
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
dependencies = [
('accounts', '0004_auto_20191121_2313'),
]
operations = [
migrations.AlterField(
model_name='token',
name='uid',
field=models.CharField(default=uuid.uuid4, max_length=40),
),
]
from django.db import models
from django.contrib.auth.models import (
AbstractBaseUser, BaseUserManager, PermissionsMixin
)
import uuid
class User(models.Model):
email = models.EmailField(primary_key=True)
REQUIRED_FIELDS = []
USERNAME_FIELD = 'email'
is_anonymous = False
is_authenticated = True
class Token(models.Model):
email = models.EmailField()
uid = models.CharField(default=uuid.uuid4, max_length=40)
class ListUserManager(BaseUserManager):
def create_user(self, email):
ListUser.objects.create(email=email)
def create_superuser(self, email, password):
self.create_user(email)
@property
def is_active(self):
return True
class ListUser(AbstractBaseUser, PermissionsMixin):
email = models.EmailField(primary_key=True)
USERNAME_FIELD = 'email'
#REQUIRED_FIELDS = ['email', 'height']
objects = ListUserManager()
@property
def is_staff(self):
return self.email == 'harry.percival@example.com'
\ No newline at end of file
<html>
<h1>Email sent</h1>
<p>Check your email, you'll find a message with a link that will log you into
the site.</p>
</html>
\ No newline at end of file
from unittest.mock import patch, call
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': 'edith@example.com'})
self.assertRedirects(response, '/')
@patch('accounts.views.send_mail')
def test_sends_mail_to_address_from_post(self, mock_send_mail):
self.client.post('/accounts/send_login_email',
data={'email': 'edith@example.com'})
self.assertTrue(mock_send_mail.called)
(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, ['edith@example.com'])
def test_adds_success_message(self):
response = self.client.post('/accounts/send_login_email',
data={'email': 'edith@example.com'},
follow=True)
message = list(response.context['messages'])[0]
self.assertEqual(
message.message,
"Check your email, we've sent you a link you can use to log in.")
self.assertEqual(message.tags, "success")
@patch('accounts.views.auth')
class LoginViewTest(TestCase):
def test_calls_authenticate_with_uid_from_get_request(self, mock_auth):
self.client.get('/accounts/login?token=abcd123')
self.assertEqual(mock_auth.authenticate.call_args, call(uid='abcd123'))
from django.contrib.auth.views import LogoutView
from django.contrib.auth.views import logout
from django.conf.urls import url
from accounts import views
urlpatterns = [
url(r'^send_login_email$', views.send_login_email, name='send_login_email'),
url(r'^login$', views.login, name='login'),
url(r'^logout$', LogoutView.as_view(next_page='/'), name='logout'),
]
\ No newline at end of file
from django.contrib import auth, messages
from django.core.mail import send_mail
from django.shortcuts import redirect
from accounts.models import Token
def send_login_email(request):
email = request.POST['email']
token = Token.objects.create(email=email)
url = request.build_absolute_uri(f'/accounts/login?token={token.uid}')
send_mail(
'Your login link for Superlists', # subject
f'Use this link to log in:\n\n{url}', # body
'noreply@superlists', # email.to
[email],
)
messages.success(
request,
"Check your email, we've sent you a link you can use to log in.")
return redirect('/')
def login(request):
user = auth.authenticate(uid=request.GET.get('token'))
if user:
auth.login(request, user)
return redirect('/')
......@@ -17,16 +17,19 @@ class FunctionalTest(StaticLiveServerTestCase):
self.options.add_argument('--dns-prefetch-disable')
self.options.add_argument('--no-sandbox')
self.options.add_argument('--headless')
self.options.add_argument('disable-gpu')
self.options.add_argument('--dns-prefetch-disable')
self.options.add_argument("--disable-gpu")
self.options.add_argument("--disable-extensions")
self.createBrowserInstance()
def createBrowserInstance(self):
try:
self.browser = webdriver.Chrome(options=self.options)
# self.browser = webdriver.Firefox()
except WebDriverException:
# linux
self.browser = webdriver.Chrome('./chromedriver',
options=self.options)
# self.browser = webdriver.Firefox('~/usr/bin/geckodriver')
self.browser.implicitly_wait(3)
......
import os
import re
from unittest import skipIf
from django.core import mail
from selenium.webdriver.common.keys import Keys
from functional_tests.base import FunctionalTest
TEST_EMAIL = 'hahaha@xmail.com'
SUBJECT = 'Your login link for Superlists'
class LoginTest(FunctionalTest):
@skipIf(os.environ.get("HEROKU_HOST"),
"Somehow always fails at Heroku, outgoing SMTP blocked by Heroku")
def test_can_get_email_link_to_log_in(self):
# Safira goes to the awesome superlists site
# and notices a "Log in" section in the navbar for the first time
# It's telling her to enter her email address, so she does
self.browser.get(self.BASE_URL)
self.browser.find_element_by_name('email').send_keys(TEST_EMAIL)
self.browser.find_element_by_name('email').send_keys(Keys.ENTER)
# A message appears telling her an email has been sent
self.wait_for(lambda: self.assertIn(
'Check your email',
self.browser.find_element_by_tag_name('body').text))
# She checks her email and finds a message
email = mail.outbox[0]
self.assertIn(TEST_EMAIL, email.to)
self.assertEqual(email.subject, SUBJECT)
# It has a url link in it
self.assertIn('Use this link to log in', email.body)
url_search = re.search(r'http://.+/.+$', email.body)
if not url_search:
self.fail(f'Could not find url in email body:\n{email.body}')
url = url_search.group(0)
self.assertIn(self.BASE_URL, url)
# she clicks it
self.browser.get(url)
# she is logged in!
self.wait_for(
lambda: self.browser.find_element_by_link_text('Log out'))
navbar = self.browser.find_element_by_css_selector('.navbar')
self.assertIn(TEST_EMAIL, navbar.text)
# Now she logs out
self.browser.find_element_by_link_text('Log out').click()
# She is logged out
self.wait_for(lambda: self.browser.find_element_by_name('email'))
navbar = self.browser.find_element_by_css_selector('.navbar')
self.assertNotIn(TEST_EMAIL, navbar.text)
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment