django入門-測試-part5

曲珂發表於2017-03-07

尊重作者的勞動,轉載請註明作者及原文地址 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&#39;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

相關文章