Fakultas Ilmu Komputer UI

Skip to content
Snippets Groups Projects
Fannyah Dita Cahya's avatar
Fannyah Dita Cahya authored
Take home programming

See merge request !28
b74535cc
History

1606918414-practice

Fannyah Dita Cahya - 1606918414 - PMPL A link heroku : https://pmpl-fannyahdita.herokuapp.com/

pipeline status

coverage report

EXERCISE-3

Pada bagian ini akan dijelaskan bagaimana proses isolation test pada aplikasi yang telah dibuat

Test Isolation

Test isolation adalah sebuah proses membagi sistem ke dalam beberapa module, sehingga program dapat di test dengan lebih mudah. Test isolation memiliki prinsip bahwa apabila suatu test dijalankan tidak akan memberikan side effect apapun.

Dengan test isolation, saat test selesai dijalankan maka environment akan di reset ke state awal. Selain itu, penggunaan database juga akan dipisahkan dari penggunaan database nyata.

Implementasi

Saat menjalankan unit test, test runner pada django akan secara otomatis membuat database test untuk menguji unit test yang terpisah dari database asli. Database test ini akan segera dihapus ketika telah selesai digunakan. Namun, untuk menjalankan functional test, masih menggunakan database nyata yang sebelumnya disimpan dengan db.sqlite3.

Pada exercise PMPL ini, functional test menggunakan class LiveServerTestCase. Class ini akan membuat database untuk test (seperti pada unit test). LiveServerTestCase dijalankan dengan test runner Django (menggunakan manage.py). Functional test dibuat menjadi suatu django app. sehingga, functional_tests.py

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.keys import Keys
import time
import unittest

class NewVisitorTest(unittest.TestCase):

    def setUp(self):
        [...]

menjadi functional_tests/tests.py

from django.test import LiveServerTestCase
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.keys import Keys
import time

class NewVisitorTest(LiveServerTestCase):

    def setUp(self):
        [...]

Setelah mengimplementasikan test isolation, functional test yang sebelumnya berbentuk long method dan meng-test segala fungsionalitas sekaligus. Sekarang functional test dapat dipisah-pisah ke beberapa method yang ingin di test. Sebelumnya di functional_tests.py

    def test_can_start_a_list_and_retrieve_it_later(self):
        self.browser.get('https://pmpl-fannyahdita.herokuapp.com')
        self.assertIn('Homepage - Fannyah Dita', self.browser.title)

        #self.assertIn('To-Do', self.browser.title)
        header_text = self.browser.find_element_by_tag_name('h2').text
        self.assertIn('To Do', header_text)

        #Have to delete db everytime runs functional test
        comment = self.browser.find_element_by_id('comment').text
        self.assertIn('Yey, waktunya berlibur', comment)

        inputbox = self.browser.find_element_by_id('id_new_item')
        self.assertEqual(
            inputbox.get_attribute('placeholder'),
            'Enter a to-do item'
        )

        inputbox.send_keys('Buy peacock feathers')
        inputbox.send_keys(Keys.ENTER)
        time.sleep(1)

        inputbox = self.browser.find_element_by_id('id_new_item')
        inputbox.send_keys('Use peacock feathers to make a fly')
        inputbox.send_keys(Keys.ENTER)
        time.sleep(1)

        comment = self.browser.find_element_by_id('comment').text
        self.assertIn('Sibuk tapi santai', comment)

        inputbox = self.browser.find_element_by_id('id_new_item')
        inputbox.send_keys('Buy chicken feathers')
        inputbox.send_keys(Keys.ENTER)
        time.sleep(1)

        inputbox = self.browser.find_element_by_id('id_new_item')
        inputbox.send_keys('Use chicken feathers to make a dust feather')
        inputbox.send_keys(Keys.ENTER)
        time.sleep(1)

        inputbox = self.browser.find_element_by_id('id_new_item')
        inputbox.send_keys('Get rest')
        inputbox.send_keys(Keys.ENTER)
        time.sleep(1)

        comment = self.browser.find_element_by_id('comment').text
        self.assertIn('Oh tidak', comment)

        self.check_for_row_in_list_table('1: Buy peacock feathers')
        self.check_for_row_in_list_table('2: Use peacock feathers to make a fly')
        self.check_for_row_in_list_table('3: Buy chicken feathers')
        self.check_for_row_in_list_table('4: Use chicken feathers to make a dust feather')
        self.check_for_row_in_list_table('5: Get rest')

Setelah diubah functional_tests/tests.py

    def test_comment_with_zero_todolist(self):
        self.browser.get(self.live_server_url)
        self.wait_for_check_comment('Yey, waktunya berlibur')

    def test_comment_with_less_than_five_todolist(self):
        self.browser.get(self.live_server_url)

        inputbox = self.browser.find_element_by_id('id_new_item')
        inputbox.send_keys('Buy peacock feathers')
        inputbox.send_keys(Keys.ENTER)

        inputbox = self.browser.find_element_by_id('id_new_item')
        inputbox.send_keys('Use peacock feathers to make a fly')
        inputbox.send_keys(Keys.ENTER)

        self.wait_for_check_comment('Sibuk tapi santai')

    def test_comment_with_more_than_five_todolist(self):
        self.browser.get(self.live_server_url)
        for i in range(6):
            inputbox = self.browser.find_element_by_id('id_new_item')
            inputbox.send_keys('Buy peacock feathers')
            inputbox.send_keys(Keys.ENTER)

        self.wait_for_check_comment('Oh tidak')

Dengan melakukan breakdown test seperti ini, dapat lebih memudahkan dalam menemukan bugs dan bugs fixing.

Referensi

"Isolation Testing". (2009, August). Accessed on September 30, 2019. Retrieved from http://www.professionalqa.com/isolation-testing

Percival, H. (2014). Test-Driven Development with Python: Obey the Testing Goat: Using Django, Selenium, and JavaScript. California: O'Reilly Media, Inc.

EXERCISE-4

Pada bagian ini akan dijelaskan bagaimana proses membuat skema desain models baru dan prettification.

Chapter 7 : Working Incrementally

Pada bagian ini, saya menambahkan skema multiple user. Sehingga, beberapa user dapat memiliki URL sendiri untuk to-do list yang ingin ditambahkan. URL yang ditambahkan berupa unique id.

lists/views.py

    def new_list(request):
        list_ = List.objects.create()
        Item.objects.create(text=request.POST['item_text'], list=list_)
        return redirect(f'/lists/{list_.id}/')

Pada chapter ini juga saya memisahkan templates untuk index.html dan list.html yang kemudaian akan dilengkapi untu prettification pada chapter 8.

Chapter 8 : Prettification

Pada bagian ini, saya mengimplementasikan styling agar homepage terlihat lebih menarik. Dengan bantuan bootsrap, saya merasa dimudahkan dalam mengatur layout agar dapat berada tengah. Pada bagian ini, ditambahkan base.html yang berguna sebagai base atau dasar dari tampilan. index.html berfungsi sebagai html yang menampilkan header dan title. Sementara, list.html akan menampilkan list to-do list.

Selain itu juga, pada chapter ini, implementasi css cukup memudahkan dalam mengatur tampilan sesuai keinginan.

contoh lists/static/base.css

#id_new_item {
    margin-top: 2ex;
}

body {
    background-color: #f5f5f5;
}
#name {
    color: #df6357;
}

Selain menambahkan css. Saya juga mencoba mengimplementasikan Sass

EXERCISE-5

Red Green Refactor dengan clean code

Penerapan Red Green Refactor cukup kental dalam implementasi TDD. Penerapan TDD dimulai dengan tahapan [RED]. Pada tahapan [RED] dibuat test sesuai harapan fungsionalitas dari implementasi suatu sistem. Pada tahapan ini, test akan failed karena implementasi belum dibuat. Kemudian, dilanjutkan dengan [GREEN] yaitu mengimplementasikan code yang benar sesuai dengan test dan fungsionalitas yang diharapkan. Pada tahapan ini, diharapkan mendapatkan hasil test yang passed. Kemudian [REFACTOR], pada tahapan ini peran clean code cukup besar. Code akan dimodifikasi sesuai dengan prinsip-prinsip clean code yang benar. Selama refactor, code harus tetap dijaga agar tetap memenuhi test.

Test Organization

Test Organizatation bermanfaat untuk memudahkan pengorganisasian berkas-berkas test dan meningkatkan readability. Dengan test organization, developer jadi lebih mudah untuk mencari suatu modules yang dibutuhkan dan dapat memudahkan untuk maintenance.

EXERCISE-6

Membuat Mutant

Pada exercise sebelumnya telah dibuat fungsi untuk menampilkan comment yang sehubungan dengan jumlah comment, seperti berikut:

    if len(items) >= 5:
        comment = "Oh tidak"
    elif len(items) >= 1:
        comment = "Sibuk tapi santai"
    else:
        comment = "Yey, waktunya berlibur"

Untuk membuat mutant, saya mencoba beberapa hal, yaitu:

  1. ketika len(items) > 5
  2. ketika len(items) > 1

Kedua mutant tersebut sebelumnya tidak di-kill sehingga harus di kill dengan membuat test cases baru.

    def test_comment_only_for_todo_equals_5(self):
        list_ = List.objects.create()
        for i in range(5):
            Item.objects.create(text='itemey {}'.format(i), list=list_)
        response = self.client.get(f'/lists/{list_.id}/')
        self.assertIn('Oh tidak', response.content.decode())   

    def test_comment_only_for_todo_equals_1(self):
        list_ = List.objects.create()
        Item.objects.create(text='itemey {}'.format(i), list=list_)
        response = self.client.get(f'/lists/{list_.id}/')
        self.assertIn('Sibuk tapi santai', response.content.decode())   

Django-Mutpy

django-mutpy merupakan modules untuk membuat mutant dan menguji apakah semua mutant telah ter-kill atau belum. Berdasarkan Django-Mutpy di dapatkan hasil

[*] Mutation score [290.71870 s]: 100.0%
   - all: 75
   - killed: 75 (100.0%)
   - survived: 0 (0.0%)
   - incompetent: 0 (0.0%)
   - timeout: 0 (0.0%)

Berdasarkan hal tersebut, telah diketahui bahwa mutant sudah 100% strongly kill.

EXERCISE 7

Spiking

Spiking merupakan implementasi prototype atau code tanpa melakukan implementasi TDD. Spiking dapat membantu untuk mengembangkan fitur baru yang sulit dan belum terlalu dipahami. Pada kasus exercise ini, mengimplementasikan log in. Spiking dilakukan di branch yang berbeda dengan implementasi sesungguhnya hanya untuk melakukan menguji apakan fungsionalitas dari log in melalui passwordless email dapat berfungsi dengan baik atau tidak.

De-Spiking

De-spiking merupakan sebuah implementasi dimana melakukan replace code yang sudah tested, dan production ready-code. Setelah implementasi diuji pada saat spiking, dilanjutkan dengan mengimplementasi ulang produk hasil spiking melalui tahapan TDD yang baik benar.

EXERCISE 8

Perbedaan Manual Mocking dan Implementasi Mock Library

Mocking dilakukan pada unittest yang akan membuat object yang mensimulasikan behaviour dari object yang sesungguhnya.

  • Manual Mocking / Monkeypatching
    Melakukan manual mocking berarti menyusun sendiri fungsi-fungsi yang ingin di mock. Untuk kasus disini, menyusun bagaimana cara melakukan test "mengirim email" tanpa benar benar mengirim email. Pada accounts/views.py terdapat bagian yang memanggil fungsi send_mail() dari django.core.mail
    def send_login_email(request):
        [...]
        send_mail(
            'Your login link for Superlists',
            message_body,
            'noreply@superlists',
            [email]
        )
        return redirect('/')

Agar saat melakukan unittest, tidak perlu benar-benar mengirimkan email, maka disusunlah sebuah fungsi untuk menggantikan fungsi send_mail() tersebut. Pada accounts/test_views.py dibuat fungsi fake_send_mail() yang dapat berfungsi sama seperti fungsi send_mail(). Kemudian setelah fake_send_mail() dipanggil, accounts.views.send_mail di assign menjadi fungsi fake_send_mail().

    def test_sends_mail_to_address_from_post(self):
        self.send_mail_called = False

        #pengganti send_mail
        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

        #assign fungsi send_mail
        accounts.views.send_mail = fake_send_mail  
        [...]
  • Menggunakan Mock Library
    Pada python, mock object library adalah unittest.mock. Hal ini dapat mempermudah proses mocking pada unittest. unittest.mock menyediakan class Mock yang dapat digunakan untuk membuat tiruan dari sebuah object. Class ini bersifat fleksibel. Sehingga pada accounts/test_view.py
    @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.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, ['edith@example.com'])

Pada exercise 8 pula digunakan fungsi patch untuk melakukan monkeypatching. Seperti contoh code di atas, patch berfungsi sama ketika mengganti fungsi send_mail() dengan fake_send_mail() pada penjelasan manual mocking di atas. Kemudian mock_send_mail sebagai argument yang akan di-inject oleh fungsi patch. Saat melakukan test, send_mail tidak akan dipanggil dan mock_send_mail yang akan dipanggil. Sehingga fungsi views tidak akan benar-benar mengirimkan email pada saat melakukan test.

Mocking membuat implementasi tightly coupled

Contohnya pada implementasi saat memanggil message success. Pada accounts/views.py memanggil fungsi messages dari django:

    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('/')

pada accounts/tests/test_views.py implementasi manual mocking akan seperti :

    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")

sementara dengan mock library akan seperti:

    @patch('accounts.views.messages')
    def test_adds_success_message_with_mocks(self, mock_messages):
        response = self.client.post('/accounts/send_login_email', data={
            'email': 'edith@example.com'
        })

        expected = "Check your email, we've sent you a link you can use to log in."
        self.assertEqual(
            mock_messages.success.call_args,
            call(response.wsgi_request, expected),
        )

Kedua fungsi melakukan pengujian yang sama. Pada kondisi views seperti ini, kedua fungsi akan passed. Namun apabila, potongan pada views diganti memanggil add_message instead of success seperti,

    messages.add_message(
        request,
        messages.SUCCESS,
        "Check your email, we've sent you a link you can use to log in."
    )

test tanpa mocking, akan tetap passed. Karena views tetap menjalankan fungsi yang seharusnya dia lakukan. Memberikan informasi pesan saat kondisi sukses. Namun, test dengan mocking library akan failed karena tidak lagi memanggil messages.success pada views pada potongan kode test yang mock_messages.success.call_args.

Hal ini yang menjadikan test dengan mock library sangat tightly coupled with karena harus benar-benar disesuaikan dengan implementasi pada code. Padahal idealnya test dilakukan untuk menguji behaviour atau sifatnya saja.

EXERCISE 9

Perbedaan Functional Test 20.1 dengan 18.3

Functional test yang dibuat pada subbab 20.1 memiliki sistem authentication sendiri untuk mengidentifikasi user. Pada subbab 20.1 FT memiliki user yang telah log in (logged in user), agar tidak perlu melakukan login email setiap kali hendak melakukan test. Untuk melakukan hal ini, dilakukan skipping login process dengan membuat ulang sebuah session. Pada functional_tests/test_my_list.py

class MyListsTest(FunctionalTest):

    def create_pre_authenticated_session(self, email):
        user = User.objects.create(email=email)
        session = SessionStore()
        session[SESSION_KEY] = user.pk #1
        session[BACKEND_SESSION_KEY] = settings.AUTHENTICATION_BACKENDS[0]
        session.save()
        ## to set a cookie we need to first visit the domain.
        ## 404 pages load the quickest!
        self.browser.get(self.live_server_url + "/404_no_such_url/")
        self.browser.add_cookie(dict(
            name=settings.SESSION_COOKIE_NAME,
            value=session.session_key, #2
            path='/',
        ))

#1. dibuat ulang session object pada database, dengan session key merupakan PK dari objek user #2. menambahkan cookie ke browser, sehingga ketika mengunjungi browser lagi server akan melihat FT ini sebagai logged in user.

Pada FT di 18.3, setiap proses FT diharuskan untuk melakukan ulang authentication dan proses log in email. Sehingga hal ini memakan waktu lebih lama dibandingkan dengan FT yang dibuat pada subbab 20.1. Sehingga FT pada 20.1 lebih baik

Deploy dan Error Functional Test (21.1 dan 21.2)

Pada saat melakukan functional test yang telah di deploy ke Heroku, terjadi error yang seperti pada instruksi:

  1. Error karena tidak dapat log in sehingga elemen 'Log out' tidak dapat ditemukan (NoSuchElement)
selenium.common.exceptions.NoSuchElementException: Message: Unable to locate
element: Log out
  1. Failed karena tidak ada elemen 'Check your Email'
AssertionError: 'Check your email' not found in 'Server Error (500)'

Hal ini disebabkan tidak email tidak dapat dikirim karena password tidak di set di environment heroku. Log pada Heroku Error yang muncul pada Log heroku adalah

2019-11-25T09:20:19.396437+00:00 app[web.1]:   File "/app/.heroku/python/lib/python3.7/smtplib.py", line 867, in sendmail
2019-11-25T09:20:19.396438+00:00 app[web.1]:     raise SMTPSenderRefused(code, resp, from_addr)
2019-11-25T09:20:19.396446+00:00 app[web.1]: smtplib.SMTPSenderRefused: (530, b'5.5.1 Authentication Required. Learn more at\n5.5.1  https://support.google.com/mail/?p=WantAuthError c37sm3626119qta.56 - gsmtp', 'noreply@superlists')

karena password dari email yang diinput belum di export, sehingga ada error ketika hendak log in.

EXERCISE 10

Pentingnya melakukan migrasi data

Data migration adalah proses pemindahan atau penggabungan data dari satu sistem ke sistem lain dengan mengubah penyimpanan, database, atau aplikasi itu sendiri. Pada exercise sebelum-sebelumnya, tidak dilakukan data migration dan kita hanya melakukan penghapusan terhadap data yang lama. Padahal dalam keaadan nyatanya bisa saja data tersebut masih dibutuhkan, dan merupakan hal yang penting. Dalam hal itu migrasi data mengambil peran yang penting.

Hal ini dilakukan dengan menambahkan unique constraint pada elemen list dan text di tabel Lists, dan menyesuaikan migrations file agar tidak ada duplikasi dan bersifat unique. lists/migrations/0002_remove_duplicates.py

def find_dupes(apps, schema_editor):
    List = apps.get_model("lists", "List")
    for list_ in List.objects.all():
        items = list_.item_set.all()
        texts = set()
        for ix, item in enumerate(items):
            if item.text in texts:
                item.text = '{} ({})'.format(item.text, ix)
                item.save()
            texts.add(item.text)

Populate data

Untuk exercise ini, dilakukan populate data sebanyak 100 data. Dibuat 10 list dengan masing-masing list memiliki 10 item.

def create_data(apps, schema_editor):
    List = apps.get_model("lists", "List")
    Item = apps.get_model("lists", "Item")
    for i in range(10):
        _list = List.objects.create()
        for j in range(10):
            data = Item.objects.create(text='dump-data-' + str(j), list=_list)