天天看點

Python Web開發-Django2.0學習07

七、自動化測試

在開發中,我們都需要測試代碼的正确性。前面的例子都是我們寫好代碼後,運作開發伺服器,在浏覽器上自己點選測試,看寫的代碼是否正常,但是這樣做很麻煩,因為以後如果有改動,可能會影響以前本來正常的功能,這樣以前的功能又得測試一遍,非常不友善,Django中有完善的單元測試,我們可以對開發的每一個功能進行單元測試,這樣隻要運作一個指令 python manage.py test,就可以測試功能是否正常。

測試就是檢查代碼是否按照自己的預期那樣運作。自動化測試的不同之處在于測試工作是由系統為您完成的。您隻需建立一組測試,然後在對應用程式進行更改時,可以檢查代碼是否仍然按照您的初始設計工作,而不必執行耗時的手動測試。

測試驅動開發: 有時候,我們知道自己需要的功能(結果),并不知道代碼如何書寫,這時候就可以利用測試驅動開發(Test Driven Development),先寫出我們期待得到的結果(把測試代碼先寫出來),再去完善代碼,直到不報錯,我們就完成了。

1、shell測試

在我們的投票應用程式中,有一個小錯誤,在polls/models.py中,Question.was_published_recently() 函數是用于判斷是否是過去最近一天内發表的,當實際上,并沒有完全考慮到。對于在将來發表的,還是傳回True。我們可以在項目環境終端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
           

2、開始自動化測試

我們在shell測試這個問題時做了什麼,這正是我們在自動化測試中所能做的,是以讓我們把它變成一個自動化的測試。

應用程式測試的正常位置是在應用程式的tests.py檔案中;測試系統會自動找到以test開頭的任何名字的測試檔案。在polls/tests.py加上如下代碼:

import datetime
from django.utils import timezone
from django.test import TestCase

from .models import Question

class QuestionModelTests(TestCase):
    def test_was_published_recently_with_future_question(self):
        """
        was_published_recently() returns 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)
           

在cmd中,進入到項目目錄,輸入下面語句開始測試:

python manage.py test polls
//python manage.py test polls.tests.QuestionModelTests
           

可以看到:

Creating test database for alias 'default'...
System check identified no issues (0 silenced).
F
======================================================================
FAIL: test_was_published_recently_with_future_question (polls.tests.QuestionModelTests)
----------------------------------------------------------------------
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'...
           

可以看到是File “/path/to/mysite/polls/tests.py”, line 16, in test_was_published_recently_with_future_question這裡出了問題,檢視上下文,發現return值那裡判斷不完整,修改polls/models.py代碼如下:

def was_published_recently(self):
now = timezone.now()
return now - datetime.timedelta(days=1) <= self.pub_date <= now
           

再次運作,可以看到:

Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.001s

OK
Destroying test database for alias 'default'...
           

我們可以進行更多的測試,在polls/tests.py輸入以下代碼進行測試:

def test_was_published_recently_with_old_question(self):
    """
    was_published_recently() returns False for questions whose pub_date
    is older than 1 day.
    """
    time = timezone.now() - datetime.timedelta(days=1, seconds=1)
    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() returns True for questions whose pub_date
    is within the last day.
    """
    time = timezone.now() - datetime.timedelta(hours=23, minutes=59, seconds=59)
    recent_question = Question(pub_date=time)
    self.assertIs(recent_question.was_published_recently(), True)
           

3、使用測試用戶端

Django提供了一個測試Client模拟使用者在視圖級别與代碼進行互動。我們可以在tests.py或者shell中使用它。我們先在shell中使用它,在那裡我們需要做一些在tests.py中不必要的事情。首先是建立測試環境,代碼如下:

>>> from django.test.utils import setup_test_environment
>>> setup_test_environment()
           

接着我們需要導入Client類,但在tests.py中我們使用django.test.TestCase替代,代碼如下:

>>> from django.test import Client
>>> #為用戶端建立一個執行個體供我們使用
>>> client = Client()
           

開始測試:

>>> # 從'/'擷取響應
>>> response = client.get('/')
Not Found: /
>>> # 我們應該從這個位址得到一個404; 如果你看到一個
>>> # "Invalid HTTP_HOST header" 錯誤和一個400響應,你可能
>>> # 省略了前面描述的setup_test_environment()調用。
>>> response.status_code
404
>>> # 另一方面,我們應該期望找到'/polls/' 
>>> # 我們将使用'reverse()'
>>> 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/" target="_blank" rel="external nofollow" >What&#39;s up?</a></li>\n    \n    </ul>\n\n'
>>> response.context['latest_question_list']
<QuerySet [<Question: What's up?>]>
           

可以發現,這裡跟前面一樣,都會獲得将來發表的questions。

4、測試ListView

修改polls/views.py的IndexView,代碼如下:

from django.utils import timezone

class IndexView(generic.ListView):
    template_name = 'polls/index.html'
    context_object_name = 'latest_question_list'

    def get_queryset(self):
    """
    傳回最近釋出的五個問題(不包括将來釋出的問題)。
    """
    return Question.objects.filter(
        pub_date__lte=timezone.now()
    ).order_by('-pub_date')[:5]
           

根據上面的shell會話建立一個測試,在polls/tests.py增加代碼,如下:

from django.urls import reverse

def create_question(question_text, days):
    """
    用給定的‘question_text’建立一個問題,并釋出給定數量的‘days’到現在的偏移量(對于
    過去釋出的問題是否定的,對于尚未釋出的問題是肯定的)。 
    """
    time = timezone.now() + datetime.timedelta(days=days)
    return Question.objects.create(question_text=question_text, pub_date=time)


class QuestionIndexViewTests(TestCase):
    def test_no_questions(self):
        """
        如果不存在問題,則顯示适當的消息。
        """
        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_past_question(self):
        ""
        過去釋出的問題,顯示在索引頁上。
        """
        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_future_question(self):
        """
        将來釋出問題,不顯示在索引頁上
        """
        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_future_question_and_past_question(self):
        """
        過去和将來釋出的問題都存在,隻顯示過去的問題。
        """
        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_two_past_questions(self):
        """
        有多個過去問題,索引頁都顯示。
        """
        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.>']
        )
           

5、測試DetailView

同樣,先修改polls/views.py的DetailView,代碼如下:

class DetailView(generic.DetailView):
    ...
    def get_queryset(self):
        """
        排除尚未釋出的問題。
        """
        return Question.objects.filter(pub_date__lte=timezone.now())
           

在polls/tests.py增加代碼,如下:

class QuestionDetailViewTests(TestCase):
    def test_future_question(self):
        """
        如果問題尚未釋出,傳回404未找到。
        """
        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_past_question(self):
        """
        如果是過去釋出的問題,顯示question_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)
           

想了解更多關于自動化測試,請查閱Testing in Django。

參考資料

  • Django官方文檔
  • 自強學堂
  • 被解放的姜戈
  • The Django Book