Fakultas Ilmu Komputer UI

Commit f3686cef authored by Rayza Arasj Mahardhika's avatar Rayza Arasj Mahardhika
Browse files

Merge branch 'exercise-5' into 'master'

Latihan 5 – Input Validation dan Test Organization

See merge request !4
parents 99588600 1905fe31
Pipeline #23417 passed with stage
in 1 minute and 50 seconds
......@@ -43,4 +43,12 @@ Dengan melakukan perubahan tersebut, To-Do List yang ada pada aplikasi selalu mu
## Exercise - 4 : Prettification
Pada bab 7 saya belajar bahwa dengan menambahkan fitur-fitur baru, fitur-fitur tersebut harus dapat dipecah agar dapat dilakukan test terhadap fitur tersebut. Tidak terkecuali jika ingin menambahkan styling atau prettification yang dilakukan pada bab 8, styling tersebut pun harus dapat dipecah agar dapat dilakukan test. Testing pada styling yang dilakukan pada bab 8 adalah mengecek apakah input berada pada tengah layar atau tidak. Untuk membuat css yang dapat menggagalkan test yang sudah ada, saya menambahkan id pada div yang membuat input tersebut berada di tengah layar, lalu saya menambahkan styiling baru dengan menggunakan id tersebut untuk membuat text berada di sebelah kiri dengan menggunakan styling `text-allign: left`.
\ No newline at end of file
Pada bab 7 saya belajar bahwa dengan menambahkan fitur-fitur baru, fitur-fitur tersebut harus dapat dipecah agar dapat dilakukan test terhadap fitur tersebut. Tidak terkecuali jika ingin menambahkan styling atau prettification yang dilakukan pada bab 8, styling tersebut pun harus dapat dipecah agar dapat dilakukan test. Testing pada styling yang dilakukan pada bab 8 adalah mengecek apakah input berada pada tengah layar atau tidak. Untuk membuat css yang dapat menggagalkan test yang sudah ada, saya menambahkan id pada div yang membuat input tersebut berada di tengah layar, lalu saya menambahkan styiling baru dengan menggunakan id tersebut untuk membuat text berada di sebelah kiri dengan menggunakan styling `text-allign: left`.
## Exercise - 5 : Test Organization
### Jelaskan keterkaitan antara penerapan refactoring (Red, Green, Refactor) dengan konsep clean code.
Red, Green, Refactor adalah tahapan-tahapan yang ada pada TDD (Test Driven Development). Red melambangkan tahap dimana kita sudah menuliskan test yang ingin kita lakukan terhadap kode kita. Green melambangkan tahap dimana kita sudah berhasil mengimplementasikan kode kita sehingga menghasilkan test yang berhasil dijalankan. Refactor melambangkan tahap dimana kita melakukan refactor terhadap kode kita yang sudah berhasil ditest (green). Keterkaitannya denga clean code adalah dengan menggunakan Red, Green, Refactor diharapkan kode yang dibuat dapat tersusun rapih dan bersih. Hal ini dikarenakan setiap proses penulisan kode yang dilakukan pasti akan ditest terlebih dahulu. Dan jika terdapat code yang kotor (code smell) akan dibersihkan pada tahap refactor.
### Jelaskan keuntungan yang dapat kita dapatkan dengan menrapkan Test Organization.
Dengan menerapkan Test Organization, test yang kita miliki akan tersusun rapih. Berbeda dengan sebelum melakukan Latihan 5, test yang ada pada repo Latihan ini hanya berada di satu file baik untuk functional_test ataupun unit_test. Hal ini akan menjadi code smell jika file yang kita miliki semakin panjang. Oleh karena itu diperlukan Test Organization agar test yang kita miliki tetap maintainable dan tidak menimbulkan code smell. Dengan pemisahan file test ini juga kita jadi dapat memilih file mana saja yang ingin kita jalankan testnya, dimana sebelumnya jika kita ingin melakukan test hanya pada suatu fungsi, kita harus menjalankan semua test yang ada pada file tersebut.
\ No newline at end of file
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
from selenium import webdriver
from selenium.common.exceptions import WebDriverException
import time
MAX_WAIT = 10
class FunctionalTest(StaticLiveServerTestCase):
def setUp(self):
self.browser = webdriver.Firefox()
self.browser.implicitly_wait(3)
def tearDown(self):
self.browser.quit()
def wait_for_row_in_list_table(self, row_text):
start_time = time.time()
while True:
try:
table = self.browser.find_element_by_id('id_list_table')
rows = table.find_elements_by_tag_name('tr')
self.assertIn(row_text, [row.text for row in rows])
return
except (AssertionError, WebDriverException) as e:
if time.time() - start_time > MAX_WAIT:
raise e
time.sleep(0.5)
def wait_for(self, fn):
start_time = time.time()
while True:
try:
return fn()
except (AssertionError, WebDriverException) as e:
raise e
time.sleep(0.5)
from .base import FunctionalTest
from selenium.webdriver.common.keys import Keys
class LayoutAndStylingTest(FunctionalTest):
def test_layout_and_styling(self):
# Edith goes to the home page
self.browser.get(self.live_server_url)
self.browser.set_window_size(1024, 768)
# She notices the input box is nicely centered
inputbox = self.browser.find_element_by_id('id_new_item')
self.assertAlmostEqual(
inputbox.location['x'] + inputbox.size['width'] / 2,
512,
delta=10
)
# She starts a new list and sees the input is nicely
# centered there too
inputbox.send_keys('testing')
inputbox.send_keys(Keys.ENTER)
self.wait_for_row_in_list_table('1: testing')
inputbox = self.browser.find_element_by_id('id_new_item')
self.assertAlmostEqual(
inputbox.location['x'] + inputbox.size['width'] / 2,
512,
delta=10
)
from .base import FunctionalTest
from selenium.webdriver.common.keys import Keys
from unittest import skip
class ItemValidationTest(FunctionalTest):
def test_cannot_add_empty_list_items(self):
# Edith goes to the home page and accidentally tries to submit
# an empty list item. She hits Enter on the empty input box
self.browser.get(self.live_server_url)
self.browser.find_element_by_id('id_new_item').send_keys(Keys.ENTER)
# The home page refreshes, and there is an error message saying
# that list items cannot be blank
self.wait_for(lambda: self.assertEqual(
self.browser.find_element_by_css_selector('.has-error').text,
"You can't have an empty list item"
))
# She tries again with some text for the item, which now works
self.browser.find_element_by_id('id_new_item').send_keys('Buy milk')
self.browser.find_element_by_id('id_new_item').send_keys(Keys.ENTER)
self.wait_for_row_in_list_table('1: Buy milk')
# Perversely, she now decides to submit a scond blank list item
self.browser.find_element_by_id('id_new_item').send_keys(Keys.ENTER)
# She receives a similar warning on the list page
self.wait_for(lambda: self.assertEqual(
self.browser.find_element_by_css_selector('.has-error').text,
"You can't have an empty list item"
))
# And she can correct it by filling some text in
self.browser.find_element_by_id('id_new_item').send_keys('Make tea')
self.browser.find_element_by_id('id_new_item').send_keys(Keys.ENTER)
self.wait_for_row_in_list_table('1: Buy milk')
self.wait_for_row_in_list_table('2: Make tea')
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
from .base import FunctionalTest
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.common.exceptions import WebDriverException
import time
MAX_WAIT = 10
class NewVisitorTest(StaticLiveServerTestCase):
def setUp(self):
self.browser = webdriver.Firefox()
self.browser.implicitly_wait(3)
def tearDown(self):
self.browser.quit()
def wait_for_row_in_list_table(self, row_text):
start_time = time.time()
while True:
try:
table = self.browser.find_element_by_id('id_list_table')
rows = table.find_elements_by_tag_name('tr')
self.assertIn(row_text, [row.text for row in rows])
return
except (AssertionError, WebDriverException) as e:
if time.time() - start_time > MAX_WAIT:
raise e
time.sleep(0.5)
class NewVisitorTest(FunctionalTest):
def test_can_start_a_list_for_one_user(self):
# Edith has heard about a cool new online to-do app. She goes
# to check out its homepage
......@@ -119,28 +97,3 @@ class NewVisitorTest(StaticLiveServerTestCase):
self.assertNotIn('make a fly', page_text)
# Satisfied, they both go back to sleep
def test_layout_and_styling(self):
# Edith goes to the home page
self.browser.get(self.live_server_url)
self.browser.set_window_size(1024, 768)
# She notices the input box is nicely centered
inputbox = self.browser.find_element_by_id('id_new_item')
self.assertAlmostEqual(
inputbox.location['x'] + inputbox.size['width'] / 2,
512,
delta=10
)
# She starts a new list and sees the input is nicely
# centered there too
inputbox.send_keys('testing')
inputbox.send_keys(Keys.ENTER)
self.wait_for_row_in_list_table('1: testing')
inputbox = self.browser.find_element_by_id('id_new_item')
self.assertAlmostEqual(
inputbox.location['x'] + inputbox.size['width'] / 2,
512,
delta=10
)
\ No newline at end of file
from django.db import models
from django.urls import reverse
class List(models.Model):
pass
def get_absolute_url(self):
return reverse('view_list', args=[self.id])
class Item(models.Model):
text = models.TextField(default='')
list = models.ForeignKey(List, default=None, on_delete=models.CASCADE)
......@@ -22,6 +22,11 @@
class="form-control input-lg"
placeholder="Enter a to-do item" />
​{% csrf_token %}
{% if error %}
<div class="form-group has-error">
<span class="help-block">{{ error }}</span>
</div>
{% endif %}
</form>
</div>
</div>
......
......@@ -2,4 +2,4 @@
{% block header_text %}Start a new To-Do list{% endblock %}
{% block form_action %}/lists/new{% endblock %}
\ No newline at end of file
{% block form_action %}{% url 'new_list' %}{% endblock %}
\ No newline at end of file
......@@ -2,7 +2,7 @@
{% block header_text %}Your To-Do list{% endblock %}
{% block form_action %}/lists/{{ list.id }}/add_item{% endblock %}
{% block form_action %}{% url 'view_list' list.id %}{% endblock %}
{% block table %}
<table id="id_list_table" class="table">
......
from django.test import TestCase
from lists.models import Item, List
from django.core.exceptions import ValidationError
class ListAndItemModelsTest(TestCase):
def test_saving_and_retrieving_items(self):
list_ = List()
list_.save()
first_item = Item()
first_item.text = 'The first (ever) list item'
first_item.list = list_
first_item.save()
second_item = Item()
second_item.text = 'Item the second'
second_item.list = list_
second_item.save()
saved_list = List.objects.first()
self.assertEqual(saved_list, list_)
saved_items = Item.objects.all()
self.assertEqual(saved_items.count(), 2)
first_saved_item = saved_items[0]
second_saved_item = saved_items[1]
self.assertEqual(first_saved_item.text, 'The first (ever) list item')
self.assertEqual(first_saved_item.list, list_)
self.assertEqual(second_saved_item.text, 'Item the second')
self.assertEqual(second_saved_item.list, list_)
def test_cannot_save_empty_list_items(self):
list_ = List.objects.create()
item = Item(list=list_, text='')
with self.assertRaises(ValidationError):
item.save()
item.full_clean()
def test_get_absolute_url(self):
list_ = List.objects.create()
self.assertEqual(list_.get_absolute_url(), f'/lists/{list_.id}/')
from django.urls import resolve, reverse
from django.test import TestCase
from django.http import HttpRequest
from django.utils.html import escape
from lists.views import home_page, about_page, \
COMMENTARY_ZERO, COMMENTARY_LESS_THAN_FIVE, COMMENTARY_MORE_THAN_EQUAL_FIVE
from lists.models import Item, List
class HomePageTest(TestCase):
def test_root_url_resolves_to_home_page_view(self):
......@@ -47,35 +48,6 @@ class AboutPageTest(TestCase):
self.assertIn('<p>I\'m currently studying Computer Science at Faculty of Computer Science, Universitas Indonesia</p>', html)
self.assertTrue(html.endswith('</html>'))
class ListAndItemModelsTest(TestCase):
def test_saving_and_retrieving_items(self):
list_ = List()
list_.save()
first_item = Item()
first_item.text = 'The first (ever) list item'
first_item.list = list_
first_item.save()
second_item = Item()
second_item.text = 'Item the second'
second_item.list = list_
second_item.save()
saved_list = List.objects.first()
self.assertEqual(saved_list, list_)
saved_items = Item.objects.all()
self.assertEqual(saved_items.count(), 2)
first_saved_item = saved_items[0]
second_saved_item = saved_items[1]
self.assertEqual(first_saved_item.text, 'The first (ever) list item')
self.assertEqual(first_saved_item.list, list_)
self.assertEqual(second_saved_item.text, 'Item the second')
self.assertEqual(second_saved_item.list, list_)
class ListViewTest(TestCase):
def test_uses_list_template(self):
......@@ -105,15 +77,12 @@ class ListViewTest(TestCase):
response = self.client.get(f'/lists/{correct_list.id}/')
self.assertEqual(response.context['list'], correct_list)
class NewItemTest(TestCase):
def test_can_save_a_POST_request_to_an_existing_list(self):
other_list = List.objects.create()
correct_list = List.objects.create()
self.client.post(
f'/lists/{correct_list.id}/add_item',
f'/lists/{correct_list.id}/',
data={'item_text': 'A new item for an existing list'}
)
......@@ -122,14 +91,38 @@ class NewItemTest(TestCase):
self.assertEqual(new_item.text, 'A new item for an existing list')
self.assertEqual(new_item.list, correct_list)
def test_redirects_to_list_view(self):
other_list = List.objects.create()
correct_list = List.objects.create()
response = self.client.post(
f'/lists/{correct_list.id}/add_item',
f'/lists/{correct_list.id}/',
data={'item_text': 'A new item for an existing list'}
)
self.assertRedirects(response, f'/lists/{correct_list.id}/')
\ No newline at end of file
self.assertRedirects(response, f'/lists/{correct_list.id}/')
def test_validation_errors_end_up_on_lists_page(self):
list_ = List.objects.create()
response = self.client.post(
f'/lists/{list_.id}/',
data={'item_text': ''}
)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'list.html')
expected_error = escape("You can't have an empty list item")
self.assertContains(response, expected_error)
class NewListTest(TestCase):
def test_validation_errors_are_sent_back_to_home_page_template(self):
response = self.client.post('/lists/new', data={'item_text': ''})
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'home.html')
expected_error = escape("You can't have an empty list item")
self.assertContains(response, expected_error)
def test_invalid_list_items_arent_saved(self):
self.client.post('/lists/new', data={'item_text': ''})
self.assertEqual(List.objects.count(), 0)
self.assertEqual(Item.objects.count(), 0)
\ No newline at end of file
......@@ -21,5 +21,4 @@ urlpatterns = [
# path('admin/', admin.site.urls),
path('new', views.new_list, name='new_list'),
path('<int:list_id>/', views.view_list, name='view_list'),
path('<int:list_id>/add_item', views.add_item, name='add_item')
]
from django.http import HttpResponse
from django.shortcuts import redirect, render
from django.core.exceptions import ValidationError
from lists.models import Item, List
COMMENTARY_ZERO = 'yey, waktunya berlibur'
......@@ -31,14 +31,27 @@ def about_page(request):
def view_list(request, list_id):
list_ = List.objects.get(id=list_id)
return render(request, 'list.html', {'list': list_})
error = None
if request.method == 'POST':
try:
item = Item(text=request.POST['item_text'], list=list_)
item.full_clean()
item.save()
return redirect(list_)
except ValidationError:
error = "You can't have an empty list item"
return render(request, 'list.html', {'list': list_, 'error': error})
def new_list(request):
list_ = List.objects.create()
Item.objects.create(text=request.POST['item_text'], list=list_)
return redirect(f'/lists/{list_.id}/')
def add_item(request, list_id):
list_ = List.objects.get(id=list_id)
Item.objects.create(text=request.POST['item_text'], list=list_)
return redirect(f'/lists/{list_.id}/')
item = Item(text=request.POST['item_text'], list=list_)
try:
item.full_clean()
item.save()
except ValidationError:
list_.delete()
error = "You can't have an empty list item"
return render(request, 'home.html', {'error': error})
return redirect(list_)
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