統計 Django 專案的測試覆蓋率
阿新 • • 發佈:2020-03-06
![](https://img2018.cnblogs.com/blog/759200/202002/759200-20200213202051843-983322874.jpg)
作者:[HelloGitHub-追夢人物](https://www.zmrenwu.com)
> 文中所涉及的示例程式碼,已同步更新到 [HelloGitHub-Team 倉庫](https://github.com/HelloGitHub-Team/HelloDjango-blog-tutorial)
我們完成了對 blog 應用和 comment 應用這兩個核心 app 的測試。現在我們想知道的是究竟測試效果怎麼樣呢?測試充分嗎?測試全面嗎?還有沒有沒有測到的地方呢?
單憑肉眼觀察難以回答上面的問題,接下來我們就藉助 [Coverage.py](https://coverage.readthedocs.io/en/coverage-5.0.3/index.html),從程式碼覆蓋率的角度來檢測一下我們的測試效果究竟如何。
[Coverage.py](https://coverage.readthedocs.io/en/coverage-5.0.3/index.html) (以下簡稱 Coverage)是 Python 測試界最為流行的一個庫之一,用來統計測試覆蓋率。測試覆蓋率可以從一個角度衡量程式碼的質量,覆蓋率越高,說明測試越充分,程式碼出現 bug 的機率也就越小。當然需要注意的是,測試覆蓋率僅僅只是衡量程式碼質量的一個角度,即使是 100% 的覆蓋率也不能說程式碼就是完美的,沒有 bug 的。
## 安裝 Coverage
要使用 Coverage,首先當然是安裝它:
```bash
$ pipenv install coverage --dev
```
因為只在開發時才用得到,所以使用 Pipenv 安裝時加 --dev 選項將其標記為開發時的依賴庫。
## 簡單配置 Coverage
Coverage 支援很多配置選項,為了方便,通常將這些配置寫在名為 `.coveragerc` 的檔案中,Coverage 執行時會從專案根目錄讀取這個配置檔案。因此先在**專案根目錄**建立這個檔案並寫入最基本的配置:
```
[run]
branch = True
source = .
[report]
show_missing = True
```
Coverage 的配置遵循 ini 檔案語法。簡單來說就是,`[section]` 代表一個配置塊,用於組織相關的一組配置。例如這裡 `[run]` 是一個配置塊,`[report]` 是另一個配置塊,兩個塊下都有相關的一些配置項。
配置項的格式為 `key = value` 。
這幾個簡單配置項的含義為:
- `branch = True`。是否統計條件語句的分支覆蓋情況。if 條件語句中的判斷通常有 True 和 False 兩種情況,設定 `branch = True` 後,Coverage 會測量這兩種情況是否都被測試到。
- `source = .`。指定需統計的原始碼目錄,這裡設定為當前目錄(即專案根目錄)。
- `show_missing = True`。在生成的統計報告中顯示未被測試覆蓋到的程式碼行號。
## 執行 Coverage
簡單配置後,我們就可以來執行 Coverage 了。
開啟命令列,進入專案根目錄,依次執行下面的命令(注意如果沒有啟用虛擬需使用 pipenv run 讓命令在虛擬環境中執行)。
首先執行 erase 命令清除上一次的統計資訊
```bash
$ pipenv run coverage erase
```
manage.py test 執行 django 單元測試,這是這一次用 coverage run 來執行
```bash
$ pipenv run coverage run manage.py test
```
生成覆蓋率統計報告
```bash
$ pipenv run coverage report
```
覆蓋率統計報告輸出如下:
```
Name Stmts Miss Branch BrPart Cover Missing
--------------------------------------------------------------------------------------------
_credentials.py 2 2 0 0 0% 1-2
blog\__init__.py 0 0 0 0 100%
blog\admin.py 11 0 0 0 100%
blog\apps.py 4 0 0 0 100%
blog\elasticsearch2_ik_backend.py 8 0 0 0 100%
blog\feeds.py 12 0 0 0 100%
blog\migrations\0001_initial.py 7 0 0 0 100%
blog\migrations\0002_auto_20190711_1802.py 7 0 0 0 100%
blog\migrations\0003_auto_20191011_2326.py 4 0 0 0 100%
blog\migrations\0004_post_views.py 4 0 0 0 100%
blog\migrations\__init__.py 0 0 0 0 100%
blog\models.py 62 0 0 0 100%
blog\search_indexes.py 8 0 0 0 100%
blog\templatetags\__init__.py 0 0 0 0 100%
blog\templatetags\blog_extras.py 15 0 0 0 100%
blog\tests\__init__.py 0 0 0 0 100%
blog\tests\test_models.py 58 0 2 0 100%
blog\tests\test_smoke.py 4 0 0 0 100%
blog\tests\test_templatetags.py 115 0 2 0 100%
blog\tests\test_utils.py 11 0 0 0 100%
blog\tests\test_views.py 170 0 8 0 100%
blog\urls.py 4 0 0 0 100%
blog\utils.py 10 0 2 1 92% 14->16
blog\views.py 40 7 2 0 79% 64-72
blogproject\__init__.py 0 0 0 0 100%
blogproject\settings\__init__.py 0 0 0 0 100%
blogproject\settings\common.py 22 0 0 0 100%
blogproject\settings\local.py 5 0 0 0 100%
blogproject\settings\production.py 5 5 0 0 0% 1-8
blogproject\urls.py 4 0 0 0 100%
blogproject\wsgi.py 4 4 0 0 0% 10-16
comments\__init__.py 0 0 0 0 100%
comments\admin.py 6 0 0 0 100%
comments\apps.py 4 0 0 0 100%
comments\forms.py 6 0 0 0 100%
comments\migrations\0001_initial.py 7 0 0 0 100%
comments\migrations\0002_auto_20191011_2326.py 4 0 0 0 100%
comments\migrations\__init__.py 0 0 0 0 100%
comments\models.py 15 0 0 0 100%
comments\templatetags\__init__.py 0 0 0 0 100%
comments\templatetags\comments_extras.py 12 0 2 0 100%
comments\tests\__init__.py 0 0 0 0 100%
comments\tests\base.py 10 0 0 0 100%
comments\tests\test_models.py 8 0 0 0 100%
comments\tests\test_templatetags.py 57 0 6 0 100%
comments\tests\test_views.py 34 0 4 0 100%
comments\urls.py 4 0 0 0 100%
comments\views.py 17 0 2 0 100%
fabfile.py 21 21 0 0 0% 1-43
manage.py 12 2 2 1 79% 11-12, 20->exit
scripts\__init__.py 0 0 0 0 100%
scripts\fake.py 63 63 14 0 0% 1-106
--------------------------------------------------------------------------------------------
TOTAL 876 104 46 2 87%
```
倒數第二列是被統計檔案的測試覆蓋率,第一列是未被覆蓋的程式碼行號。
大部分檔案測試覆蓋率為 100%,說明我們的測試還是比較充分的。但從報告結果中我們發現這樣幾個問題:
1. 有一些檔案其實並不需要測試,或者並非專案的核心檔案(例如部署指令碼 fabfile.py,django 的 migrations 檔案等),這些檔案應該從統計中排除。
2. Coverage 預設顯示全部檔案的覆蓋率統計結果,如果檔案比較多的話就不好查詢非 100% 覆蓋率的檔案。畢竟我們的目標是提高程式碼覆蓋率,因此已達 100% 覆蓋的程式碼檔案我們不再關心。我們要做的是找到非 100% 覆蓋率的檔案,為其新增缺失的測試。
## 完善 Coverage 配置
可以通過新增 Coverage 配置項輕鬆解決上面 2 個問題。
在 `[run]` 配置塊中增加 `omit` 配置項可以指定排除統計的檔案。
在 `[report]` 配置塊中增加 `skip_covered` 配置項可以指定統計報告中不顯示 100% 覆蓋的檔案。
這是 `.coveragerc` 最終配置結果,注意我們在 omit 配置項中指定忽略了一些非核心的專案檔案:
```
[run]
branch = True
source = .
omit =
_credentials.py
manage.py
blogproject/settings/*
fabfile.py
scripts/fake.py
*/migrations/*
blogproject\wsgi.py
[report]
show_missing = True
skip_covered = True
```
再次按照上一節所說的方式執行 Coverage,最終報告結果如下:
```
Name Stmts Miss Branch BrPart Cover Missing
-----------------------------------------------------------
blog\utils.py 10 0 2 1 92% 14->16
blog\views.py 40 7 2 0 79% 64-72
-----------------------------------------------------------
TOTAL 709 7 30 1 99%
33 files skipped due to complete coverage.
```
這個報告指出我們仍有 2 個檔案沒有達到 100% 的覆蓋率,我們要做的就是為這兩個檔案中未測試的程式碼增加單元測試,讓其達到 100% 測試覆蓋率。
不過在動手寫測試之前,我們要搞清楚哪些程式碼沒被測到。命令列報告的最後一列指出了未被測試程式碼的行號,但是這樣看著不是很直觀。一種體驗更好的方式是生成 HTML 報告,這樣我們可以直接在 HTML 報告中檢視到未被測試到的具體程式碼。
## 生成 HTML 報告
`coverage report` 命令在命令列生成統計報告,而 `coverage html` 則可以生成 HTML 報告。
在上一節的基礎上,執行如下命令:
```bash
$ pipenv run coverage html
```
執行完成後專案根目錄會多出一個 htmlcov 的資料夾,裡面就是測試覆蓋率的 HTML 報告檔案。用瀏覽器開啟裡面的 index.html 檔案就可以檢視報告結果了:
![](https://img2020.cnblogs.com/blog/759200/202003/759200-20200305135059863-1454372220.png)
主頁和命令列的結果是一樣的,不過我們可以點選檔名,進入到對這個檔案更加具體的統計報告頁面,例如 **blog\views.py** 結果如下:
![](https://img2020.cnblogs.com/blog/759200/202003/759200-20200305135117451-702760735.png)
綠色部分代表已覆蓋的程式碼,紅色部分代表為覆蓋的程式碼。
## 完善單元測試
檢視檔案我們發現,**blog\views.py** 中未被覆蓋的程式碼原來是 [Django 部落格實現簡單的全文搜尋](https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/85/) 中的程式碼,現在我們已經將搜尋替換為 [Django Haystack 全文檢索](https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/86/) 了,這段程式碼也就不需要了,可以直接刪除。
**blog\views.py** 的報告結果則表明我們在 [Django Haystack 全文檢索與關鍵詞高亮](https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/86/) 中自定義的搜尋關鍵詞高亮器有一個 if 分支條件未被測試到:
![](https://img2020.cnblogs.com/blog/759200/202003/759200-20200305135125866-1113873626.png)
檢查 blog/tests/test_utils.py 中的測試用例,我們發現只測試了比較短的標題不被截斷,也就是
```python
if len(text_block) < self.max_length:
```
判斷條件為 True,缺失對判斷條件為 False 的測試。所以我們來構造一個新的測試用例測試標題長度超過 `max_length` (預設值為 200)的情況時會被截斷:
```python
class HighlighterTestCase(TestCase):
def test_highlight(self):
# 省略已有程式碼 ...
highlighter = Highlighter("標題")
document = "這是一個長度超過 200 的標題,應該被截斷。" + "HelloDjangoTutorial" * 200
self.assertTrue(
highlighter.highlight(document).startswith(
'...標題,應該被截斷。'
)
)
```
再次執行 Coverage 生成報告,測試覆蓋率全都 100% 了!
```bash
$ pipenv run coverage erase
$ pipenv run coverage run manage.py test
$ pipenv run coverage report
# 輸出
Name Stmts Miss Branch BrPart Cover Missing
---------------------------------------------------
---------------------------------------------------
TOTAL 704 0 28 0 100%
```
最後提醒一點,Coverage 執行後可能會在專案目錄下生成一些檔案,這些檔案並不需要納入版本管理,所以將其加入 .gitignore 檔案中,防止被提交到程式碼庫:
```
htmlcov/
.coverage
.coverage.*
coverage.xml
*.cover
```
## HelloDjango 往期回顧:
第 30 篇:[Django 部落格單元測試:測試評論應用](https:////www.cnblogs.com/xueweihan/p/12372401.html)
第 29 篇:[編寫 Django 應用單元測試](https:////www.cnblogs.com/xueweihan/p/12336462.html)
第 28 篇:[Django Haystack 全文檢索與關鍵詞高亮](https://www.cnblogs.com/xueweihan/p/12304983.html)
---
![](https://img2018.cnblogs.com/blog/759200/202002/759200-20200213201956024-782757549.png)
**關注公眾號加入交