尊重作者的勞動,轉載請註明作者及原文地址 http://www.cnblogs.com/txwsqk/p/6515996.html
完全翻譯自官方文件 https://docs.djangoproject.com/en/1.10/intro/tutorial05/
前面好幾段文字都是介紹什麼是自動化測試,我們為什麼需要寫測試用例,還有現在流行的"測試驅動開發",總之有如下好處:
- 節省你的時間
你修改了工程,設計好多模組,如果沒有自動化測試,好吧,手動一個個測吧 - 測試不能發現問題,但是能阻止問題的發生
沒有測試用例,那麼你程式的行為就是不可預見的,即使這是你寫的程式碼,你實際也不知道它內部是怎麼執行的;有了測試用例,當某一個地方出問題,它就能指出這部分有問題,即使你自己壓根沒有意識到這裡有問題 - 測試可以讓你的程式碼更有吸引力
當你寫了一段很牛[A-Z][1]的程式碼,但是別人並不想看,因為你沒有測試用例,那麼別人就認為你的程式碼是不可信的 - 測試讓團隊合作更愉快
小的應用可能只有一個開發者,但是大部分複雜的應用是一個團隊一起開發的,測試用例可以防止你的同事不小心干擾了你的程式碼
好了,總之就是為你的工程寫測試 百利無一害
在前面章節中我們是怎麼驗證程式碼的,通過django-admin的shell
>>> import datetime >>> from django.utils import timezone >>> from polls.models import Question >>> # create a Question instance with pub_date 30 days in the future >>> future_question = Question(pub_date=timezone.now() + datetime.timedelta(days=30)) >>> # was it published recently? >>> future_question.was_published_recently() True
好了下面我們用django自帶的測試功能--自動化測試
在你的應用目錄下有一個 test.py 就是它
django的自動化測試系統會在你的應用目錄下找test開頭的檔案然後執行它
我們往test.py裡寫個測試用例
import datetime from django.utils import timezone from django.test import TestCase from .models import Question class QuestionMethodTests(TestCase): def test_was_published_recently_with_future_question(self): """ was_published_recently() should return False for questions whose pub_date is in the future. """ time = timezone.now() + datetime.timedelta(days=30) future_question = Question(pub_date=time) self.assertIs(future_question.was_published_recently(), False)
執行一下
python manage.py test polls
執行這個命令都發生了什麼呢
這個類繼承了TestCase, 它會建立一個測試用的資料庫,這個類的方法都應該是test開頭的方法
執行結果如下
Creating test database for alias 'default'... F ====================================================================== FAIL: test_was_published_recently_with_future_question (polls.tests.QuestionMethodTests) ---------------------------------------------------------------------- Traceback (most recent call last): File "/path/to/mysite/polls/tests.py", line 16, in test_was_published_recently_with_future_question self.assertIs(future_question.was_published_recently(), False) AssertionError: True is not False ---------------------------------------------------------------------- Ran 1 test in 0.001s FAILED (failures=1) Destroying test database for alias 'default'...
顯然測試失敗了
我們建立了一個問題,這個問題的釋出時間是30天后,那麼我們顯然希望was_published_recently()返回False
但是測試用例卻返回了True,所以我們程式碼有bug,我們修復一下polls/models.py
def was_published_recently(self): now = timezone.now() return now - datetime.timedelta(days=1) <= self.pub_date <= now
在重新執行下
python manage.py test polls
好了這回測試通過了
Creating test database for alias 'default'... . ---------------------------------------------------------------------- Ran 1 test in 0.001s OK Destroying test database for alias 'default'...
現在我們修復了 was_published_recently()的bug,我們來加幾個複雜的測試用例
def test_was_published_recently_with_old_question(self): """ was_published_recently() should return False for questions whose pub_date is older than 1 day. """ time = timezone.now() - datetime.timedelta(days=30) old_question = Question(pub_date=time) self.assertIs(old_question.was_published_recently(), False) def test_was_published_recently_with_recent_question(self): """ was_published_recently() should return True for questions whose pub_date is within the last day. """ time = timezone.now() - datetime.timedelta(hours=1) recent_question = Question(pub_date=time) self.assertIs(recent_question.was_published_recently(), True)
下面我們來測試檢視函式
我們在建立question時,如果pub_date是未來的時間,那麼按理說這不應該在頁面顯示的,就像京東明天12:00有搶購活動,那麼在這個時間點之前是不應該讓使用者看到的
在繼續講之前,先介紹一個django的測試命令 django.test.Client, 它可以在檢視層模擬使用者的互動行為,我們可以用Client命令在test.py或shell中
>>> from django.test.utils import setup_test_environment >>> setup_test_environment()
這個命令會載入模板渲染功能,這樣我們就可以檢驗一些額外的屬性比如response.context
注意這個命令不會去建立測試資料庫,它用真實的資料庫資料做測試
還有不要忘了在settings.py裡設定正確的時區TIME_ZONE
Client怎麼用呢
>>> from django.test import Client >>> # create an instance of the client for our use >>> client = Client()
>>> # get a response from '/' >>> response = client.get('/') >>> # we should expect a 404 from that address >>> response.status_code 404 >>> # on the other hand we should expect to find something at '/polls/' >>> # we'll use 'reverse()' rather than a hardcoded URL >>> from django.urls import reverse >>> response = client.get(reverse('polls:index')) >>> response.status_code 200 >>> response.content b'\n <ul>\n \n <li><a href="/polls/1/">What's up?</a></li>\n \n </ul>\n\n' >>> # If the following doesn't work, you probably omitted the call to >>> # setup_test_environment() described above >>> response.context['latest_question_list'] <QuerySet [<Question: What's up?>]>
好了回到正題,修復我們的index檢視,之前的index檢視是這樣的,沒有判斷question的釋出時間是否是小於現在的時間
class IndexView(generic.ListView): template_name = 'polls/index.html' context_object_name = 'latest_question_list' def get_queryset(self): """Return the last five published questions.""" return Question.objects.order_by('-pub_date')[:5]
修正這個問題,當pub_date小於現在的時間時不顯示
def get_queryset(self): """ Return the last five published questions (not including those set to be published in the future). """ return Question.objects.filter( pub_date__lte=timezone.now() ).order_by('-pub_date')[:5]
來測試一下這個新檢視吧
def create_question(question_text, days): """ Creates a question with the given `question_text` and published the given number of `days` offset to now (negative for questions published in the past, positive for questions that have yet to be published). """ time = timezone.now() + datetime.timedelta(days=days) return Question.objects.create(question_text=question_text, pub_date=time) class QuestionViewTests(TestCase): def test_index_view_with_no_questions(self): """ If no questions exist, an appropriate message should be displayed. """ response = self.client.get(reverse('polls:index')) self.assertEqual(response.status_code, 200) self.assertContains(response, "No polls are available.") self.assertQuerysetEqual(response.context['latest_question_list'], []) def test_index_view_with_a_past_question(self): """ Questions with a pub_date in the past should be displayed on the index page. """ create_question(question_text="Past question.", days=-30) response = self.client.get(reverse('polls:index')) self.assertQuerysetEqual( response.context['latest_question_list'], ['<Question: Past question.>'] ) def test_index_view_with_a_future_question(self): """ Questions with a pub_date in the future should not be displayed on the index page. """ create_question(question_text="Future question.", days=30) response = self.client.get(reverse('polls:index')) self.assertContains(response, "No polls are available.") self.assertQuerysetEqual(response.context['latest_question_list'], []) def test_index_view_with_future_question_and_past_question(self): """ Even if both past and future questions exist, only past questions should be displayed. """ create_question(question_text="Past question.", days=-30) create_question(question_text="Future question.", days=30) response = self.client.get(reverse('polls:index')) self.assertQuerysetEqual( response.context['latest_question_list'], ['<Question: Past question.>'] ) def test_index_view_with_two_past_questions(self): """ The questions index page may display multiple questions. """ create_question(question_text="Past question 1.", days=-30) create_question(question_text="Past question 2.", days=-5) response = self.client.get(reverse('polls:index')) self.assertQuerysetEqual( response.context['latest_question_list'], ['<Question: Past question 2.>', '<Question: Past question 1.>'] )
講解:
create_question()函式讓我們方便的建立question
上面的幾個函式根據函式名就能知道是幹什麼的,不解釋了
index檢視修復完了,detail檢視也得改
class DetailView(generic.DetailView): ... def get_queryset(self): """ Excludes any questions that aren't published yet. """ return Question.objects.filter(pub_date__lte=timezone.now())
一樣的邏輯,當pub_date less than equeal 小於等於timezone.now()現在的時間則不顯示
為detail寫測試用例
class QuestionIndexDetailTests(TestCase): def test_detail_view_with_a_future_question(self): """ The detail view of a question with a pub_date in the future should return a 404 not found. """ future_question = create_question(question_text='Future question.', days=5) url = reverse('polls:detail', args=(future_question.id,)) response = self.client.get(url) self.assertEqual(response.status_code, 404) def test_detail_view_with_a_past_question(self): """ The detail view of a question with a pub_date in the past should display the question's text. """ past_question = create_question(question_text='Past Question.', days=-5) url = reverse('polls:detail', args=(past_question.id,)) response = self.client.get(url) self.assertContains(response, past_question.question_text)
繼續總結
你會發現當你開始寫測試用例,你的測試程式碼就瘋狂的增加了,而且他們很多重複的內容,相比你寫的程式碼,測試程式碼是那麼的醜陋,這不要緊,因為大部分時間你都不會察覺到他們的存在,你可以繼續你的開發任務,不過當你修改了檢視邏輯,你也要同步更新你的測試用例,不然會有一大頓測試用例過不了
關於寫測試用例,下面有幾條好的建議:
- 一個模型或檢視一個單獨的測試類
- 每個測試的方法只測試一種情況
- 測試方法的名字要顧名思義
擴充套件一下
我們現在只介紹了基本的測試功能,當你想測試瀏覽器的行為時可以使用 "Selenium"測試框架,這個框架不光能測試你的程式碼,還有你的javascript,django也包含了一個工具LiveServerTestCase來讓你使用Selenium
當你的功能很複雜時,你可能希望當你提交程式碼時能自動執行測試,那麼你去關注一下"持續整合"相關的內容
現在很多ide可以檢查你的程式碼覆蓋率,用這個方法也能找出你的哪些程式碼還沒有被測試到,這也能讓你發現哪些程式碼已經沒用了可以刪掉了.
如果你發現有的程式碼無法被測試,很明顯這段程式碼應該被重構或者刪除.有個工具Coverage可以幫你識別無用的程式碼 參考https://docs.djangoproject.com/en/1.10/topics/testing/advanced/#topics-testing-code-coverage