02.21 編寫 Django 應用單元測試

編寫 Django 應用單元測試

作者:HelloGitHub-追夢人物

文中所涉及的示例代碼,已同步更新到 HelloGitHub-Team 倉庫

我們博客功能越來越來完善了,但這也帶來了一個問題,我們不敢輕易地修改已有功能的代碼了!

我們怎麼知道代碼修改後帶來了預期的效果?萬一改錯了,不僅新功能沒有用,原來已有的功能都可能被破壞。此前我們開發一個新的功能,都是手工運行開發服務器去驗證,不僅費時,而且極有可能驗證不充分。

如何不用每次開發了新的功能或者修改了已有代碼都得去人工驗證呢?解決方案就是編寫自動化測試,將人工驗證的邏輯編寫成腳本,每次新增或修改代碼後運行一遍測試腳本,腳本自動幫我們完成全部測試工作。

接下來我們將進行兩種類型的測試,一種是單元測試,一種是集成測試。

單元測試是一種比較底層的測試,它將一個功能邏輯的代碼塊視為一個單元(例如一個函數、方法、或者一個 if 語句塊等,單元應該儘可能小,這樣測試就會更加充分),程序員編寫測試代碼去測試這個單元,確保這個單元的邏輯代碼按照預期的方式執行了。通常來說我們一般將一個函數或者方法視為一個單元,對其進行測試。

集成測試則是一種更加高層的測試,它站在系統角度,測試由各個已經經過充分的單元測試的模塊組成的系統,其功能是否符合預期。

我們首先來進行單元測試,確保各個單元的邏輯都沒問題後,然後進行集成測試,測試整個博客系統的可用性。

Python 一般使用標準庫 unittest 提供單元測試,django 拓展了單元測試,提供了一系列類,用於不同的測試場合。其中最常用到的就是 django.test.TestCase 類,這個類和 Python 標準庫的 unittest.TestCase 類似,只是拓展了以下功能:

  • 提供了一個 client 屬性,這個 client 是 Client 的實例。可以把 Client 看做一個發起 HTTP 請求的功能庫(類似於 requests),這樣我們可以方便地使用這個類測試視圖函數。
  • 運行測試前自動創建數據庫,測試運行完畢後自動銷燬數據庫。我們肯定不希望自動生成的測試數據影響到真實的數據。

博客應用的單元測試,主要就是和這個類打交道。

django 應用的單元測試包括:

  • 測試 model,model 的方法是否返回了預期的數據,對數據庫的操作是否正確。
  • 測試表單,數據驗證邏輯是否符合預期
  • 測試視圖,針對特定類型的請求,是否返回了預期的響應
  • 其它的一些輔助方法或者類等

接下來我們就逐一地來測試上述內容。

搭建測試環境

測試寫在 tests.py 裡(應用創建時就會自動創建這個文件),首先來個冒煙測試,用於驗證測試功能是否正常,在 blog\\tests.py 文件寫入如下代碼:


編寫 Django 應用單元測試


使用 manage.py 的 test 命令將自動發現 django 應用下的 tests 文件或者模塊,並且自動執行以 test_ 開頭的方法。運行:pipenv run python manage.py test

Creating test database for alias 'default'... System check identified no issues (0 silenced).

.

-------------------------------------------------------

Ran 1 test in 0.002s

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

OK 表明我們的測試運行成功。

不過,如果需要測試的代碼比較多,把全部測試邏輯一股腦塞入 tests.py,這個模塊就會變得十分臃腫,不利於維護,所以我們把 tests.py 文件升級為一個包,不同的單元測試寫到包下對應的模塊中,這樣便於模塊化地維護和管理。

刪除 blog\\tests.py 文件,然後在 blog 應用下創建一個 tests 包,再創建各個單元測試模塊:


編寫 Django 應用單元測試

  • test_models.py 存放和模型有關的單元測試
  • test_views.py 測試視圖函數
  • test_templatetags.py 測試自定義的模板標籤
  • test_utils.py 測試一些輔助方法和類等

注意

tests 包中的各個模塊必須以 test_ 開頭,否則 django 無法發現這些測試文件的存在,從而不會運行裡面的測試用例。

測試模型

模型需要測試的不多,因為基本上都是使用了 django 基類 models.Model 的特性,自己的邏輯很少。拿最為複雜的 Post 模型舉例,它包括的邏輯功能主要有:

  • __str__ 方法返回 title 用於模型實例的字符表示
  • save 方法中設置文章創建時間(created_time)和摘要(exerpt)
  • get_absolute_url 返回文章詳情視圖對應的 url 路徑
  • increase_views 將 views 字段的值 +1

單元測試就是要測試這些方法執行後的確返回了上面預期的結果,我們在 test_models.py 中新增一個類,叫做 PostModelTestCase,在這個類中編寫上述單元測試的用例。


編寫 Django 應用單元測試

這裡代碼雖然比較多,但做的事情很明確。setUp 方法會在每一個測試案例運行前執行,這裡做的事情是在數據庫中創建一篇文章,用於測試。

接下來的各個 test_* 方法就是對於各個功能單元的測試,以 test_auto_populate_modified_time 為例,這裡我們要測試文章保存到數據庫後,modifited_time 被正確設置了值(期待的值應該是文章保存時的時間)。

self.assertIsNotNone(self.post.modified_time) 斷言文章的 modified_time 不為空,說明的確設置了值。TestCase 類提供了系列 assert* 方法用於斷言測試單元的邏輯結果是否和預期相符,一般從方法的命名中就可以讀出其功能,比如這裡 assertIsNotNone 就是斷言被測試的變量值不為 None。

接著我們嘗試通過

<code>self.post.body = '新的測試內容'
self.post.save()/<code>

修改文章內容,並重新保存數據庫。預期的結果應該是,文章保存後,modifited_time 的值也被更新為修改文章時的時間,接下來的代碼就是對這個預期結果的斷言:

<code>self.post.refresh_from_db()
self.assertTrue(self.post.modified_time > old_post_modified_time)/<code>

這個 refresh_from_db 方法將刷新對象 self.post 的值為數據庫中的最新值,然後我們斷言數據庫中 modified_time 記錄的最新時間比原來的時間晚,如果斷言通過,說明我們更新文章後,modified_time 的值也進行了相應更新來記錄修改時間,結果符合預期,測試通過。

其它的測試方法都是做著類似的事情,這裡不再一一講解,請自行看代碼分析。

測試視圖

視圖函數測試的基本思路是,向某個視圖對應的 URL 發起請求,視圖函數被調用並返回預期的響應,包括正確的 HTTP 響應碼和 HTML 內容。

我們的博客應用包括以下類型的視圖需要進行測試:

  • 首頁視圖 IndexView,訪問它將返回全部文章列表。
  • 標籤視圖,訪問它將返回某個標籤下的文章列表。如果訪問的標籤不存在,返回 404 響應。
  • 分類視圖,訪問它將返回某個分類下的文章列表。如果訪問的分類不存在,返回 404 響應。
  • 歸檔視圖,訪問它將返回某個月份下的全部文章列表。
  • 詳情視圖,訪問它將返回某篇文章的詳情,如果訪問的文章不存在,返回 404。
  • 自定義的 admin,添加文章後自動填充 author 字段的值。
  • RSS,返回全部文章的 RSS 內容。

首頁視圖、標籤視圖、分類視圖、歸檔視圖都是同一類型的視圖,他們預期的行為應該是:

  • 返回正確的響應碼,成功返回200,不存在則返回404。
  • 沒有文章時正確地提示暫無文章。
  • 渲染了正確的 html 模板。
  • 包含關鍵的模板變量,例如文章列表,分頁變量等。

我們首先來測試這幾個視圖。為了給測試用例生成合適的數據,我們首先定義一個基類,預先定義好博客的數據內容,其它視圖函數測試用例繼承這個基類,就不需要每次測試時都創建數據了。我們創建的測試數據如下:

  • 分類一、分類二
  • 標籤一、標籤二
  • 文章一,屬於分類一和標籤一,文章二,屬於分類二,沒有標籤
編寫 Django 應用單元測試

以 CategoryViewTestCase 為例:


編寫 Django 應用單元測試

這個類首先繼承自 BlogDataTestCase,setUp 方法別忘了調用父類的 stepUp 方法,以便在每個測試案例運行時,設置好博客測試數據。

然後就是進行了3個案例測試:

  • 訪問一個不存在的分類,預期返回 404 響應碼。
  • 沒有文章的分類,返回200,但提示暫時還沒有發佈的文章!渲染的模板為 index.html
  • 訪問的分類有文章,則響應中應該包含系列關鍵的模板變量,post_list、is_paginated、page_obj,post_list 文章數量為1,因為我們的測試數據中這個分類下只有一篇文章,post_list 是一個 queryset,預期是該分類下的全部文章,時間倒序排序。

其它的 TagViewTestCase 等測試類似,請自行參照代碼分析。

博客文章詳情視圖的邏輯更加複雜一點,所以測試用例也更多,主要需要測試的點有:

  • 訪問不存在文章,返回404。
  • 文章每被訪問一次,訪問量 views 加一。
  • 文章內容被 markdown 渲染,並生成了目錄。

測試代碼如下:


編寫 Django 應用單元測試


接下來是測試 admin 添加文章和 rss 訂閱內容,這一塊比較簡單,因為大部分都是 django 的邏輯,django 已經為我們進行了測試,我們需要測試的只是自定義的部分,確保自定義的邏輯按照預期的定義運行,並且得到了預期的結果。

對於 admin,預期的結果就是發佈文章後,的確自動填充了 author:


編寫 Django 應用單元測試

  • reverse('admin:blog_post_add') 獲取 admin 管理添加博客文章的 URL,django admin 添加文章的視圖函數名為 admin:blog_post_add,一般 admin 後臺操作模型的視圖函數命名規則是 _<model>_<action>。/<action>/<model>
  • self.client.login(username=self.user.username, password='admin') 登錄用戶,相當於後臺登錄管理員賬戶。
  • self.client.post(self.url, data=data) ,向添加文章的 url 發起 post 請求,post 的數據為需要發佈的文章內容,只指定了 title,body和分類。

接著我們進行一系列斷言,確認是否正確創建了文章。

RSS 測試也類似,我們期待的是,它返回的內容中的確包含了全部文章的內容:


編寫 Django 應用單元測試

測試模板標籤

這裡測試的核心內容是,模板中 {% templatetag %} 被渲染成了正確的 HTML 內容。你可以看到測試代碼中對應的代碼:


編寫 Django 應用單元測試

注意模板標籤本質上是一個 Python 函數,第一句代碼中我們直接調用了這個函數,由於它需要接受一個 Context 類型的標量,因此我們構造了一個空的 context 給它,調用它將返回需要的上下文變量,然後我們構造了一個需要的上下文變量。

接著我們構造了一個模板對象。

最後我們使用構造的上下文去渲染了這個模板。

我們調用了模板引擎的底層 API 來渲染模板,視圖函數會渲染模板,返回響應,但是我們沒有看到這個過程,是因為 django 幫我們在背後的調用了這個過程。

全部模板引擎的測試套路都是一樣,構造需要的上下文,構造模板,使用上下文渲染模板,斷言渲染的模板內容符合預期。以為例:


編寫 Django 應用單元測試

這個模板標籤對應側邊欄的最新文章版塊。我們進行了2處關鍵性的內容斷言。一個是包含最新文章版塊標題,一個是內容中含有文章標題的超鏈接。

測試輔助方法和類

我們的博客中只自定義了關鍵詞高亮的一個邏輯。


編寫 Django 應用單元測試

這裡 Highlighter 實例化時接收搜索關鍵詞作為參數,然後 highlight 將搜索結果中關鍵詞包裹上 span 標籤。

Highlighter 事實上 haystack 為我們提供的類,我們只是定義了 highlight 方法的邏輯。我們又是如何知道 highlight 方法的邏輯呢?如何進行測試呢?

我是看源碼,大致瞭解了 Highlighter 類的實現邏輯,然後我從 haystack 的測試用例中找到了 highlight 的測試方法。

所以,有時候不要懼怕去看源代碼,Python 世界裡一切都是開源的,源代碼也沒有什麼神秘的地方,都是人寫的,別人能寫出來,你學習後也一樣能寫出來。單元測試的代碼一般比較冗長重複,但目的也十分明確,而且大都以順序邏輯組織,代碼自成文檔,非常好讀。

單純看文章中的講解你可能仍有迷惑,但是好好讀一遍示例項目中測試部分的源代碼,你一定會對單元測試有一個更加清晰的認識,然後依葫蘆畫瓢,寫出對自己項目代碼的單元測試。


[1]HelloGitHub-追夢人物: https://www.zmrenwu.com

[2]HelloGitHub-Team 倉庫: https://github.com/HelloGitHub-Team/HelloDjango-blog-tutorial

編寫 Django 應用單元測試

『講解開源項目系列』——讓對開源項目感興趣的人不再畏懼、讓開源項目的發起者不再孤單。跟著我們的文章,你會發現編程的樂趣、使用和發現參與開源項目如此簡單。歡迎留言聯繫我們、加入我們,讓更多人愛上開源、貢獻開源~


分享到:


相關文章: