[Python+Django]初心者筆記14(Testing a Django web application)
本篇教學的部分是
如何自動化的測試Django web application
從之前的文章一步一步進行過來的話,將會在程式碼看到這個檔案locallibrary\catalog\tests.py
不用懷疑,將他刪除(或是重新命名也可以)!
接著打開此檔案/catalog/tests/test_models.py,內容加入:
from django.test import TestCase
# Create your tests here.
並且將以下的class加入到同一個檔案中:
class YourTestClass(TestCase):
@classmethod
#執行下面的method之前,會先執行一次setUpTestData(cls)來新增假資料
def setUpTestData(cls):
print("setUpTestData: Run once to set up non-modified data for all class methods.")
pass
def setUp(self):
#下面每個method測試之前都會執行一次setup(self),作為初始化
print("setUp: Run once for every test method to setup clean data.")
pass
def test_false_is_false(self):
print("Method: test_false_is_false.")
self.assertFalse(False)
def test_false_is_true(self):
print("Method: test_false_is_true.")
self.assertTrue(False)
def test_one_plus_one_equals_two(self):
print("Method: test_one_plus_one_equals_two.")
self.assertEqual(1 + 1, 2)
接著於終端機執行下列指令就可以執行測試了:
python manage.py test
可以看到測試的結果有一項出錯,FAIL: test_false_is_true,當然false跟true是不同的,這是我們故意要讓他測試出錯的:
如果希望測試的output能顯示的詳細一點,可以把上面終端機的指令改為:
python manage.py test --verbosity 2
如果想執行一部分的測試就好,不要測試全部的話,可參考下面的終端機指令:
可以只測試某資料夾、或是某個檔案、或是某個TestClass、或是某個method
python manage.py test catalog.tests # Run the specified module
python manage.py test catalog.tests.test_models # Run the specified module
python manage.py test catalog.tests.test_models.YourTestClass # Run the specified class
python manage.py test catalog.tests.test_models.YourTestClass.test_one_plus_one_equals_two # Run the specified method
將剛才的/catalog/tests/test_models.py的內容全部改為如下,正式開始測試Locallibrary網站:
from django.test import TestCase
# Create your tests here.
from catalog.models import Author
class AuthorModelTest(TestCase):
@classmethod
def setUpTestData(cls):
#Set up non-modified objects used by all test methods
#新增一筆假資料做測試
Author.objects.create(first_name='Big', last_name='Bob')
def test_first_name_label(self):
author=Author.objects.get(id=1)
#檢查欄位的別名是否有被改掉
field_label = author._meta.get_field('first_name').verbose_name
self.assertEquals(field_label,'first name')
def test_date_of_death_label(self):
author=Author.objects.get(id=1)
field_label = author._meta.get_field('date_of_death').verbose_name
self.assertEquals(field_label,'died')
def test_first_name_max_length(self):
author=Author.objects.get(id=1)
#檢查欄位的maxlength是否被改掉
max_length = author._meta.get_field('first_name').max_length
self.assertEquals(max_length,100)
def test_object_name_is_last_name_comma_first_name(self):
author=Author.objects.get(id=1)
#檢查 資料表.tostring是否被改掉
expected_object_name = '%s, %s' % (author.last_name, author.first_name)
self.assertEquals(expected_object_name,str(author))
def test_get_absolute_url(self):
author=Author.objects.get(id=1)
#This will also fail if the urlconf is not defined.
self.assertEquals(author.get_absolute_url(),'/catalog/author/1')
並請再執行終端機指令測試一次,得到結果如下:
發現Died與died並不相同,因此測出一個錯誤!
接著示範測試forms.py的方式,首先在/catalog/tests/test_forms.py加入:
以下內容很簡單,因此就沒有註解
from django.test import TestCase
# Create your tests here.
import datetime
from django.utils import timezone
from catalog.forms import RenewBookForm
class RenewBookFormTest(TestCase):
def test_renew_form_date_field_label(self):
form = RenewBookForm()
self.assertTrue(form.fields['renewal_date'].label == None or form.fields['renewal_date'].label == 'renewal date')
def test_renew_form_date_field_help_text(self):
form = RenewBookForm()
self.assertEqual(form.fields['renewal_date'].help_text,'Enter a date between now and 4 weeks (default 3).')
def test_renew_form_date_in_past(self):
date = datetime.date.today() - datetime.timedelta(days=1)
form_data = {'renewal_date': date}
form = RenewBookForm(data=form_data)
self.assertFalse(form.is_valid())
def test_renew_form_date_too_far_in_future(self):
date = datetime.date.today() + datetime.timedelta(weeks=4) + datetime.timedelta(days=1)
form_data = {'renewal_date': date}
form = RenewBookForm(data=form_data)
self.assertFalse(form.is_valid())
def test_renew_form_date_today(self):
date = datetime.date.today()
form_data = {'renewal_date': date}
form = RenewBookForm(data=form_data)
self.assertTrue(form.is_valid())
def test_renew_form_date_max(self):
date = timezone.now() + datetime.timedelta(weeks=4)
form_data = {'renewal_date': date}
form = RenewBookForm(data=form_data)
self.assertTrue(form.is_valid())
執行測試之後,發現沒有測出任何錯誤:
接著示範如何測試views.py,請於/catalog/tests/test_views.py加入下列內容:
from django.test import TestCase
# Create your tests here.
from catalog.models import Author
from django.urls import reverse
from django.contrib.auth.models import User #Required to login
class AuthorListViewTest(TestCase):
@classmethod
def setUpTestData(cls):
#Create 13 authors for pagination tests
number_of_authors = 13
for author_num in range(number_of_authors):
Author.objects.create(first_name='Christian %s' % author_num, last_name = 'Surname %s' % author_num,)
#因為"all authors"網頁 = /catalog/authors/ 需要登入
#因此新增使用者來登入
def setUp(self):
#Create two users
test_user1 = User.objects.create_user(username='testuser1', password='12345')
test_user1.save()
self.client.login(username='testuser1', password='12345')
def test_view_url_exists_at_desired_location(self):
resp = self.client.get('/catalog/authors/')
self.assertEqual(resp.status_code, 200)
def test_view_url_accessible_by_name(self):
resp = self.client.get(reverse('authors'))
self.assertEqual(resp.status_code, 200)
def test_view_uses_correct_template(self):
resp = self.client.get(reverse('authors'))
self.assertEqual(resp.status_code, 200)
self.assertTemplateUsed(resp, 'catalog/author_list.html')
def test_pagination_is_ten(self):
resp = self.client.get(reverse('authors'))
self.assertEqual(resp.status_code, 200)
self.assertTrue('is_paginated' in resp.context)
self.assertTrue(resp.context['is_paginated'] == True)
self.assertTrue( len(resp.context['author_list']) == 10)
def test_lists_all_authors(self):
#Get second page and confirm it has (exactly) remaining 3 items
resp = self.client.get(reverse('authors')+'?page=2')
self.assertEqual(resp.status_code, 200)
self.assertTrue('is_paginated' in resp.context)
self.assertTrue(resp.context['is_paginated'] == True)
self.assertTrue( len(resp.context['author_list']) == 3)
在test_view.py裡面也可以作出比較複雜的假資料,幫假資料的每個欄位都設定特定value:
#練習做出比較作出比較複雜的假資料,幫假資料的每個欄位都設定特定value
import datetime
from django.utils import timezone
from catalog.models import BookInstance, Book, Genre, Language
from django.contrib.auth.models import User #Required to assign User as a borrower
class LoanedBookInstancesByUserListViewTest(TestCase):
def setUp(self):
#Create two users
test_user1 = User.objects.create_user(username='testuser1', password='12345')
test_user1.save()
test_user2 = User.objects.create_user(username='testuser2', password='12345')
test_user2.save()
#Create a book
test_author = Author.objects.create(first_name='John', last_name='Smith')
test_genre = Genre.objects.create(name='Fantasy')
test_language = Language.objects.create(name='English')
test_book = Book.objects.create(title='Book Title', summary = 'My book summary', isbn='ABCDEFG', author=test_author, language=test_language)
# Create genre as a post-step
genre_objects_for_book = Genre.objects.all()
test_book.genre.set(genre_objects_for_book) #Direct assignment of many-to-many types not allowed.
test_book.save()
#Create 30 BookInstance objects
number_of_book_copies = 30
for book_copy in range(number_of_book_copies):
return_date= timezone.now() + datetime.timedelta(days=book_copy%5)
if book_copy % 2:
the_borrower=test_user1
else:
the_borrower=test_user2
status='m'
BookInstance.objects.create(book=test_book,imprint='Unlikely Imprint, 2016', due_back=return_date, borrower=the_borrower, status=status)
def test_redirect_if_not_logged_in(self):
resp = self.client.get(reverse('my-borrowed'))
self.assertRedirects(resp, '/accounts/login/?next=/catalog/mybooks/')
def test_logged_in_uses_correct_template(self):
login = self.client.login(username='testuser1', password='12345')
resp = self.client.get(reverse('my-borrowed'))
#Check our user is logged in
self.assertEqual(str(resp.context['user']), 'testuser1')
#Check that we got a response "success"
self.assertEqual(resp.status_code, 200)
#Check we used correct template
self.assertTemplateUsed(resp, 'catalog/bookinstance_list_borrowed_user.html')
在test_view.py裡面也可以針對權限做測試:
#測試 有權限的使用者 才能夠 編輯BookInstance資料
from django.contrib.auth.models import Permission # Required to grant the permission needed to set a book as returned.
class RenewBookInstancesViewTest(TestCase):
def setUp(self):
#Create a user
test_user1 = User.objects.create_user(username='testuser1', password='12345')
test_user1.save()
#testuser2將會有權限編輯BookInstance的權限
test_user2 = User.objects.create_user(username='testuser2', password='12345')
test_user2.save()
permission = Permission.objects.get(codename='can_view_all_borrowed_books')
#這樣寫也可以
# permission = Permission.objects.get(name='one can view all borrowed books')
test_user2.user_permissions.add(permission)
test_user2.save()
#Create a book
test_author = Author.objects.create(first_name='John', last_name='Smith')
test_genre = Genre.objects.create(name='Fantasy')
test_language = Language.objects.create(name='English')
test_book = Book.objects.create(title='Book Title', summary = 'My book summary', isbn='ABCDEFG', author=test_author, language=test_language,)
# Create genre as a post-step
genre_objects_for_book = Genre.objects.all()
test_book.genre.set(genre_objects_for_book) # Direct assignment of many-to-many types not allowed.
test_book.save()
#Create a BookInstance object for test_user1
return_date= datetime.date.today() + datetime.timedelta(days=5)
self.test_bookinstance1=BookInstance.objects.create(book=test_book,imprint='Unlikely Imprint, 2016', due_back=return_date, borrower=test_user1, status='o')
#Create a BookInstance object for test_user2
#testuser2才有權限編輯他借的這本BookInstance
return_date= datetime.date.today() + datetime.timedelta(days=5)
self.test_bookinstance2=BookInstance.objects.create(book=test_book,imprint='Unlikely Imprint, 2016', due_back=return_date, borrower=test_user2, status='o')
def test_redirect_if_not_logged_in(self):
resp = self.client.get(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}) )
#Manually check redirect (Can't use assertRedirect, because the redirect URL is unpredictable)
self.assertEqual( resp.status_code,302)
self.assertTrue( resp.url.startswith('/accounts/login/') )
def test_redirect_if_logged_in_but_not_correct_permission(self):
login = self.client.login(username='testuser1', password='12345')
resp = self.client.get(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}) )
#Manually check redirect (Can't use assertRedirect, because the redirect URL is unpredictable)
self.assertEqual( resp.status_code,302)
self.assertTrue( resp.url.startswith('/accounts/login/') )
def test_logged_in_with_permission_borrowed_book(self):
login = self.client.login(username='testuser2', password='12345')
resp = self.client.get(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance2.pk,}) )
#Check that it lets us login - this is our book and we have the right permissions.
self.assertEqual( resp.status_code,200)
def test_logged_in_with_permission_another_users_borrowed_book(self):
login = self.client.login(username='testuser2', password='12345')
resp = self.client.get(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}) )
#Check that it lets us login. We're a librarian, so we can view any users book
self.assertEqual( resp.status_code,200)
def test_HTTP404_for_invalid_book_if_logged_in(self):
import uuid
test_uid = uuid.uuid4() #unlikely UID to match our bookinstance!
login = self.client.login(username='testuser2', password='12345')
resp = self.client.get(reverse('renew-book-librarian', kwargs={'pk':test_uid,}) )
# print('resp.status_code:' + str(resp.status_code))
# print('resp.url:' + str(resp.url))
self.assertEqual( resp.status_code,404)
def test_uses_correct_template(self):
login = self.client.login(username='testuser2', password='12345')
resp = self.client.get(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}) )
self.assertEqual( resp.status_code,200)
#Check we used correct template
self.assertTemplateUsed(resp, 'catalog/book_renew_librarian.html')
def test_form_renewal_date_initially_has_date_three_weeks_in_future(self):
login = self.client.login(username='testuser2', password='12345')
#test_bookinstance1竟然可以直接代表該資料表的第一個物件FirstOrDefault(),好神!
resp = self.client.get(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}) )
self.assertEqual( resp.status_code,200)
date_3_weeks_in_future = datetime.date.today() + datetime.timedelta(weeks=3)
#resp.context['form'].initial['renewal_date']用來取得form上面的物件value
self.assertEqual(resp.context['form'].initial['renewal_date'], date_3_weeks_in_future )
def test_redirects_to_all_borrowed_book_list_on_success(self):
login = self.client.login(username='testuser2', password='12345')
valid_date_in_future = datetime.date.today() + datetime.timedelta(weeks=2)
resp = self.client.post(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}), {'renewal_date':valid_date_in_future} )
self.assertRedirects(resp, reverse('all-borrowed') )
def test_form_invalid_renewal_date_past(self):
login = self.client.login(username='testuser2', password='12345')
date_in_past = datetime.date.today() - datetime.timedelta(weeks=1)
resp = self.client.post(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}), {'renewal_date':date_in_past} )
self.assertEqual( resp.status_code,200)
#assertFormError表示此測試必定會發生錯誤
self.assertFormError(resp, 'form', 'renewal_date', 'Invalid date - renewal in past')
def test_form_invalid_renewal_date_future(self):
login = self.client.login(username='testuser2', password='12345')
invalid_date_in_future = datetime.date.today() + datetime.timedelta(weeks=5)
resp = self.client.post(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}), {'renewal_date':invalid_date_in_future} )
self.assertEqual( resp.status_code,200)
self.assertFormError(resp, 'form', 'renewal_date', 'Invalid date - renewal more than 4 weeks ahead')
個人見解本篇最重要的一點:
在每個系統功能完成的時候,利用python內建的test framework或是selenium來做自動化的測試最大的好處就是,當以後新增或是修改大量的系統功能之後,工程師只要輕鬆的按下"開始測試"的按鍵,然後回家睡覺,明天一大早就可以知道最近的修改是否影響到系統的正常運作!不用在那邊手動測試老半天!!
然後隔天沒問題的話就可以把最新版的程式碼發行上去了!
參考資料:
Django Tutorial Part 10: Testing a Django web application
https://developer.mozilla.org/en-US/docs/Learn/Server-side/Django/Testing