Python 測試驅動開發(四)測試及重構的目的(上)

使用Selenium測試用戶交互

如果用繩子從井 裏提一桶水,井不太深,而且桶不是很滿,提起來很容易。就算提滿滿一桶水,剛開始也很容易。但用不了多久你就累了。TDD 理念好比是一個棘輪,你可以使用它保存當前的進度,休息一會兒,而且能保證進度絕不倒退。

上一章,我們進行到哪裏了,忘記了,可以執行命令來看看
注意:從本章開始,我的目錄從lists改爲list,因爲之前的環境不小心刪掉了,從新搞了遍環境

進入目錄運行 functional_test.py

$ python functional_tests.py

如果這裏報錯,提示說加載頁面出錯或者無法連接。
這是因爲運行測試之前沒有使用manage.py runserver 啓動開發服務器。
啓動服務

python manage.py runserver

TDD 的優點之一是,永遠不會忘記接下該做什麼——重新運行測試就知道要接下來要做的事情。

在這裏插入圖片描述
斷言報錯返回,Finish the test! 。
我們打開並編輯functional_tests.py 文件,完成功能測試用例

from selenium import webdriver
from selenium.webdriver.common.keys import Keys
import time
import unittest


class NewVisitorTest(unittest.TestCase):
    def setUp(self):
        self.brower = webdriver.Firefox()

    def tearDown(self):
        self.brower.quit()

    def test_can_start_a_list_and_retrieve_it_later(self):
        # 伊迪絲聽說有一個很酷的在線待辦事項應用
        # 她去看了這個應用的首頁
        self.brower.get('http://localhost:8000')

        # 她注意到網頁的標題和頭部都包含“To-Do”這個詞
        self.assertIn('To-Do', self.brower.title)
        header_test = self.brower.find_element_by_tag_name('h1').text  # 1
        self.assertIn('To-Do', header_test)

        # 應用邀請她輸入一個待辦事項
        inputbox = self.brower.find_element_by_id('id_new_item')  # 1
        self.assertEqual(
            inputbox.get_attribute('placeholder'),
            'Enter a to-do item'
        )

        # 她在一個文本框中輸入了“Buy peacock feathers”(購買孔雀羽毛)
        # 伊迪絲的愛好是使用假蠅做魚餌釣魚
        inputbox.send_keys('Buy peacock feathers')  # 2

        # 她按回車鍵後,頁面更新了
        # 待辦事項表格中顯示了“1: Buy peacock feathers”
        inputbox.send_keys(Keys.ENTER)  # 3
        time.sleep(1)  # 4
        table = self.brower.find_element_by_id('id_list_table')
        rows = table.find_element_by_tag_name('tr')  # 1
        self.assertTrue(
            any(row.text == '1: Buy peacock feathers' for row in rows)
        )
        
        # 頁面中又顯示了一個文本框,可以輸入其他的待辦事項
        # 她輸入了“Use peacock feathers to make a fly”(使用孔雀羽毛做假蠅)
        # 伊迪絲做事很有條理
        # 頁面再次更新,她的清單中顯示了這兩個待辦事項
        # 伊迪絲想知道這個網站是否會記住她的清單
        # 她看到網站爲她生成了一個唯一的URL
        self.fail('Finish the test!')
        
        # 頁面再次更新,她的清單中顯示了這兩個待辦事項
        # 伊迪絲想知道這個網站是否會記住她的清單
        # 她看到網站爲她生成了一個唯一的URL

if __name__ == '__main__':
    unittest.main(warnings='ignore')

註釋裏用到的方法如下

  1. 使用了Selenium提供的幾個方法用來查找網頁的內容:
    find_element_by_tag_name
    find_elements_by_tag_name
    注意第二個元素多了一個s會返回多個元素,也可能返回一個空的列表

  2. 使用了Selenium提供的send_keys方法用來在輸入框輸入內容

  3. Keys這個類(需要導入) 的作用是讓我們發送回車鍵的按鍵

  4. 按下回車鍵會刷新。time.sleep(),表示暫停幾秒鐘

  5. any函數,這是Python裏的原生函數

    any() 函數用於判斷給定的可迭代參數 iterable 是否全部爲 False,則返回 False,如果有一個爲 True,則返回 True。

繼續運行python functional_tests.py,報錯:Message: Unable to locate element: h1。找不到h1元素
在這裏插入圖片描述
在對Web應用修改之前,我們進行一次提交

$ git diff # 會顯示對functional_tests.py的改動
$ git commit -am "Functional test now checks we can input a to-do item"

遵守“不測試常量”規則,使用模板解決這個問題

我們去看下list/tests.py中的單元測試。測試時要查找特定的HTML字符串,但是這個方法效率很低。所以,單元測試的規則之一是不測試常量,我們以文本形式測試HTML在很大程度上就是測試常量
如果有以下的代碼:

   wibble = 3

在測試時,就沒有必要按照以下方法寫

from myprogram import wibble
assert wibble == 3

單元測試的本質是邏輯、流程控制和配置。我們編寫斷言測試HTML字符串中是否有指定的字符串,這個不是單元測試應該做的

在Python代碼中插入原始字符串需要是處理HTML錯誤的。我們有更好的方法,使用模板。如果把HTML放在一個擴展名爲.html文件中。Python領域有很多模板框架,Django也有自己的模板系統,而且很好用,我們來看看

使用模板重構

讓視圖函數返回完全一樣的HTML,但是使用不同的處理方式。這個過程叫作重構,在功能不變的前提下改進代碼
重構的原則是不能沒有測試。我們在做測試驅動開發,測試已經有了,現在只需要檢查下測試是否通過,測試通過後才能保證重構前後的表現一致
先把HTML 字符串提取出來寫入單獨的文件。創建模板的文件夾list/templates並新建文件list/templates/home.html,再把HTML 寫入這個文件
這裏我用的notepad++有高亮語法

<html>
	<title>To-Do list</title>
</html>

在這裏插入圖片描述
接下來,我們修改視圖,讓它去調用home.html:

from django.shortcuts import render

def home_page(request):
    return render(request,'home.html')

在這裏插入圖片描述
現在我們不用自己構建HttpResponse對象,可以使用Django中的render函數。

render

第一個參數是請求對象(後面介紹)
第二個參數是渲染的模板名
Django會自動在所有的應用目錄中搜索名爲templates的文件夾,然後根據模板中的內容構建一個HttpResponse 對象

模板是Django 中一個很強大的功能,使用模板的主要優勢之一是能把Python 變量代入HTML 文本
這也是爲什麼使用render 和render_to_string(稍後用到),而不用原生的open 函數手動從硬盤中讀取模板文件的原因

現在我們測試下模板,看下模板是否生效

$ python manage.py test

在這裏插入圖片描述
運行報錯:TemplateDoesNotExist: home.html

我們剛纔有寫home.html模板並且放在了list/templates文件夾中。找不到的原因是沒有在Django中註冊list應用,需要用startapp 命令添加應用
執行startapp 命令後,還需要配置應用到文件settings.py 中,打開settings.py,找到變量INSTALLED_APPS,把list 目錄加進去:

# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'list',	# 添加
]

然後再運行測試

$ python manage.py test

在這裏插入圖片描述
測試通過,至此我們對代碼做的小重構通過了測試,從現在開始我們不需要測試常量,只需要檢查模板渲染是否正確

Django測試客戶端

測試模板渲染是否正確,一種方法是在測試中手動渲染模板,然後與視圖返回的結果做比較
在這裏我們利用Django 提供的render_to_string 函數
打開並編輯單元測試文件list/tests.py

from django.template.loader import render_to_string
[...]
def test_home_page_returns_correct_html(self):
request = HttpRequest()
response = home_page(request)
html = response.content.decode('utf8')
expected_html = render_to_string('home.html')
self.assertEqual(html, expected_html)

運行測試,通過

$ python manage.py test

在這裏插入圖片描述
這樣測試有點麻煩,測試時調用.decode() 和.strip()又測試太麻煩,所以選擇使用Django自帶的測試客戶端(TestClient)檢查使用那個模板的原生方式。
我們從新修改list/tests.py,來看看效果

class HomePageTest(TestCase):
    def test_root_url_resolves_to_home_page_view(self):
        found = resolve('/')
        self.assertEqual(found.func, home_page)

    def test_home_page_returns_correct_html(self):
        response = self.client.get('1')     # 1

        html = response.content.decode('utf8')  # 2
        self.assertTrue(html.startswith('<html>'))
        self.assertIn('<title>To-Do list</title>', html)
        self.assertTrue(html.strip().endswith('</html>'))
        self.assertTemplateUsed(response, 'home.html')  # 3
  1. 不用手動創建HttpRequest 對象,也不用直接調用視圖函數,而是調用self.client.get方法直接傳入要測試的URL
  2. 暫時保留
  3. .assertTemplateUsed 是Django TestCase 類提供的測試方法,用於檢查響應是使用哪個模板渲染(注意,這個方法只能測試通過測試客戶端獲取的響應)

運行測試

$ python manage.py test

在這裏插入圖片描述
測試通過後,我們對我們的測試結果有疑慮,所以我們需要搞點小事情
打開並編輯單元測試文件list/tests.py

self.assertTemplateUsed(response, 'home.html')

修改爲

self.assertTemplateUsed(response, 'wrong.html')

運行測試

$ python manage.py test

在這裏插入圖片描述
我們再將斷言修改回去,並且對測試Case進行了精簡,順便把原來的 test_root_url_resolves 測試Case刪除,因爲Django的測試客戶端已經後臺測試過來了,我們將2個麻煩的測試Case精簡成一個

from django.test import TestCase


class HomePageTest(TestCase):
    def test_uses_home_template(self):
        response = self.client.get("/")
        self.assertTemplateUsed(response, "home.html")

運行測試

$ python manage.py test

在這裏插入圖片描述

關於重構

這個重構的例子很煩瑣。但正如Kent Beck 在Test-Driven Development: By Example 一書中所說的:“我是推薦你在實際工作中這麼做嗎?不是。我只是建議你要知道怎麼按照這種 方式做。”
其實,寫這一部分時我的第一反應是先修改代碼,直接使用assertTemplateUsed 函數,刪除那三個多餘的斷言,只在渲染得到的結果中檢查期望看到的內容,然後再修改代碼。
但要注意,如果真這麼做了可能就會犯錯,因爲我可能不會在模板中編寫正確的 和

標籤,而是隨便寫一些字符串。

重構後,我們做一次提交

$ git status # 會看到tests.py、views.py、settings.py以及新建的templates文件夾
$ git add . # 還會添加尚未跟蹤的templates文件夾
$ git diff --staged # 審查我們想提交的內容
$ git commit -m "Refactor home page view to use a template"

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章