測試是伴隨着開發進行的,開發有多久,測試就要多久。本教程已經進行了30多章了,都是如何測試的?固然是runserver
啦!每當開發新功能後,都須要運行服務器,僞裝本身就是用戶,測試是否運行正常。html
這樣的人工測試優勢是很是直觀,你看到的和用戶看到的是徹底相同的。可是缺點也很明顯:前端
爲了解決人工測試的種種問題,Django
引入了Python
標準庫的單元測試模塊,也就是自動化測試了:你能夠寫一段代碼,讓代碼幫你測試!(程序員是最會偷懶的職業..)代碼會忠實的完成測試任務,幫助你從繁重的測試工做中解脫出來。除此以外,自動化測試還有如下優勢:python
雖然學習自動化測試不會讓你的博客增長一絲絲的功能,可是能夠讓代碼更增強壯,因此我以爲頗有必要拿出一章來專門講講。git
Django官方文檔的第5部分講測試講得很是的好,而且有中文版本。本章節就大量借鑑了官方文檔,也很是很是推薦讀者去拜讀。程序員
爲了演示測試是如何工做的,讓咱們首先在文章模型中寫個有bug的方法:github
article/models.py
from django.utils import timezone
class ArticlePost(models.Model):
...
def was_created_recently(self):
# 若文章是"最近"發表的,則返回 True
diff = timezone.now() - self.created
if diff.days <= 0 and diff.seconds < 60:
return True
else:
return False
複製代碼
這個方法用於檢測當前文章是不是最近發表的。數據庫
這個方法稍微擴展一下就會變得很是實用。好比能夠將博文的發表日期顯示爲「剛剛」、「3分鐘前」、「5小時前」等相對時間,用戶體驗將大有提高。django
仔細看看,它是沒辦法正確判斷「將來」的文章的:編程
>>> import datetime
>>> from django.utils import timezone
>>> from article.models import ArticlePost
>>> from django.contrib.auth.models import User
# 建立一篇"將來"的文章
>>> future_article = ArticlePost(author=User(username='user'), title='test',body='test', created=timezone.now() + datetime.timedelta(days=30))
# 是不是「最近」發表的?
>>> future_article.was_created_recently()
True
複製代碼
將來發生的確定不是最近發生的,所以代碼是錯誤的。後端
接下來就要寫測試用例,將測試轉爲自動化。
還記得最初生成文章app時候的目錄結構嗎?
article
│ admin.py
│ apps.py
│ models.py
│ tests.py
│ views.py
│ __init__.py
│
└─migrations
└─ __init__.py
複製代碼
這個tests.py
就是留給你寫測試用例的地方了:
article/tests.py
from django.test import TestCase
import datetime
from django.utils import timezone
from article.models import ArticlePost
from django.contrib.auth.models import User
class ArticlePostModelTests(TestCase):
def test_was_created_recently_with_future_article(self):
# 若文章建立時間爲將來,返回 False
author = User(username='user', password='test_password')
author.save()
future_article = ArticlePost(
author=author,
title='test',
body='test',
created=timezone.now() + datetime.timedelta(days=30)
)
self.assertIs(future_article.was_created_recently(), False)
複製代碼
基本就是把剛纔在Shell
中的測試代碼抄了過來。有點不一樣的是末尾這個assertIs
方法,瞭解**「斷言」**的同窗會對它很熟悉:它的做用是檢測方法內的兩個參數是否徹底一致,若是不是則拋出異常,提醒你這個地方是有問題滴。
接下來運行測試:
(env) > python manage.py test
複製代碼
運行結果以下:
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
F
======================================================================
FAIL: test_was_created_recently_with_future_article (article.tests.ArticlePostModelTests)
----------------------------------------------------------------------
Traceback (most recent call last):
File "E:\django_project\my_blog\article\tests.py", line 19, in test_was_created_recently_with_future_article
self.assertIs(future_article.was_created_recently(), False)
AssertionError: True is not False
----------------------------------------------------------------------
Ran 1 test in 0.000s
FAILED (failures=1)
Destroying test database for alias 'default'...
複製代碼
這裏面名堂就不少了:
tests
開頭的文件中尋找測試代碼TestCase
的子類都被認爲是測試代碼test
開頭的方法會被認爲是測試用例assertIs
拋出異常,由於True is not False
測試系統明確指明瞭錯誤的數量、位置和種類等信息,請讀者細細品嚐。
既然經過測試找到了bug,那接下來就要把代碼進行修正:
article/models.py
from django.utils import timezone
class ArticlePost(models.Model):
...
def was_created_recently(self):
diff = timezone.now() - self.created
# if diff.days <= 0 and diff.seconds < 60:
if diff.days == 0 and diff.seconds >= 0 and diff.seconds < 60:
return True
else:
return False
複製代碼
從新運行測試:
(env) > python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
Destroying test database for alias 'default'...
複製代碼
此次代碼順利經過了測試。
能夠確定的是,在日後的開發中,這個bug不會再出現了,由於你只須要運行一遍測試,就會當即獲得警告。能夠認爲項目的這一小部分代碼永遠是安全的。
既然一個測試用例就能夠保證一小段代碼永遠安全,那我寫一堆測試豈不是能夠保證整個項目永遠安全嗎?確實如此,這個買賣絕對是不虧的。
所以咱們繼續再增長几個測試,全面強化代碼:
article/tests.py
...
from django.test import TestCase
import datetime
from django.utils import timezone
from article.models import ArticlePost
from django.contrib.auth.models import User
class ArticlePostModelTests(TestCase):
def test_was_created_recently_with_future_article(self):
# 若文章建立時間爲將來,返回 False
...
def test_was_created_recently_with_seconds_before_article(self):
# 若文章建立時間爲 1 分鐘內,返回 True
author = User(username='user1', password='test_password')
author.save()
seconds_before_article = ArticlePost(
author=author,
title='test1',
body='test1',
created=timezone.now() - datetime.timedelta(seconds=45)
)
self.assertIs(seconds_before_article.was_created_recently(), True)
def test_was_created_recently_with_hours_before_article(self):
# 若文章建立時間爲幾小時前,返回 False
author = User(username='user2', password='test_password')
author.save()
hours_before_article = ArticlePost(
author=author,
title='test2',
body='test2',
created=timezone.now() - datetime.timedelta(hours=3)
)
self.assertIs(hours_before_article.was_created_recently(), False)
def test_was_created_recently_with_days_before_article(self):
# 若文章建立時間爲幾天前,返回 False
author = User(username='user3', password='test_password')
author.save()
months_before_article = ArticlePost(
author=author,
title='test3',
body='test3',
created=timezone.now() - datetime.timedelta(days=5)
)
self.assertIs(months_before_article.was_created_recently(), False)
複製代碼
如今咱們擁有了4個測試,來保證was_created_recently()
方法對於過去、最近、將來中的4種狀況都返回正確的值。你還能夠繼續擴展,直到你以爲徹底沒有任何bug藏匿的可能性爲止。
在實際的開發中,有些難纏的bug會把本身假裝得很是的好,而不是像教程這樣明確的知道它就在那裏。有了自動化測試,不管之後你的項目怎麼變化、app交互多麼的複雜,只要在測試中寫好的邏輯就必定是符合預期的,而你所須要作的只是運行一條測試指令而已。
雖然教程中僅使用了
assertIs
,但實際上Django中的斷言有大概幾十種之多,好比assertEqual
、assertContains
等,而且還在不斷更新。詳見Python標準斷言和Django擴展斷言
上面的測試都是針對模型的。視圖該怎麼測試?如何經過測試系統模擬出用戶的請求呢?
答案是TestCase
類提供了一個供測試使用的Client
來模擬用戶經過請求和視圖層代碼的交互。
以文章詳情視圖的瀏覽量統計爲例,比較容易出現的潛在bug有:
updated
字段也錯誤的一併更新因此有針對的寫2條測試。新寫一個專門測試視圖的類,與前面的測試模型的類區分開:
article/tests.py
...
from time import sleep
from django.urls import reverse
class ArticlePostModelTests(TestCase):
...
class ArtitclePostViewTests(TestCase):
def test_increase_views(self):
# 請求詳情視圖時,閱讀量 +1
author = User(username='user4', password='test_password')
author.save()
article = ArticlePost(
author=author,
title='test4',
body='test4',
)
article.save()
self.assertIs(article.total_views, 0)
url = reverse('article:article_detail', args=(article.id,))
response = self.client.get(url)
viewed_article = ArticlePost.objects.get(id=article.id)
self.assertIs(viewed_article.total_views, 1)
def test_increase_views_but_not_change_updated_field(self):
# 請求詳情視圖時,不改變 updated 字段
author = User(username='user5', password='test_password')
author.save()
article = ArticlePost(
author=author,
title='test5',
body='test5',
)
article.save()
sleep(0.5)
url = reverse('article:article_detail', args=(article.id,))
response = self.client.get(url)
viewed_article = ArticlePost.objects.get(id=article.id)
self.assertIs(viewed_article.updated - viewed_article.created < timezone.timedelta(seconds=0.1), True)
複製代碼
注意看代碼是如何與視圖層交互的:response = self.client.get(url)
向視圖發起請求並得到了響應,剩下的就是從數據庫中取出更新後的數據,並用斷言語句來判斷代碼是否符合預期了。
運行測試:
(env) > python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
......
----------------------------------------------------------------------
Ran 6 tests in 0.617s
OK
Destroying test database for alias 'default'...
複製代碼
6條測試用例所有經過。
僅僅是app中的兩個很是小的功能,就已經寫了6條測試用例了,而且還能夠繼續擴展。除此以外,其餘的每一個模型、視圖均可以擴展出幾十甚至上百條測試,這樣下去代碼總量很快就要失去控制了,而且相對於業務代碼來講,測試代碼顯得繁瑣且不夠優雅。
**可是不要緊!**就讓測試代碼繼續肆意增加吧。大部分狀況下,你寫完一個測試以後就能夠忘掉它了。在你繼續開發的過程當中,它會一直默默無聞地爲你作貢獻的。最壞的狀況是當你繼續開發的時候,發現以前的一些測試如今看來是多餘的。可是這也不是什麼問題,多作些測試也不錯。
在前面的測試中,咱們已經從模型層和視圖層的角度檢查了應用的輸入輸出,可是模板呢?雖然能夠用assertInHTML
、assertJSONEqual
等斷言大體檢查模板中的某些內容,但更加近似於瀏覽器的檢查就要使用Selenium
等測試工具(畢竟Django的重點是後端而不是前端)。
Selenium
不只能夠測試 Django 框架裏的代碼,甚至還能夠檢查 JavaScript代碼。它僞裝成是一個正在和你站點進行交互的瀏覽器,就好像有個真人在訪問網站同樣。Django 提供了LiveServerTestCase
來和Selenium
這樣的工具進行交互。
關於測試的話題這裏只是開了個頭,讀者能夠繼續閱讀下面的內容進一步瞭解:
有一幫崇尚「測試驅動」的開發者,他們開發時先寫測試代碼,而後才寫業務代碼。而普通開發者一般是先寫業務代碼,再寫測試代碼,這也是沒問題的。但若是你已經寫了不少業務代碼了,再回頭寫測試確實有些無從下手,那麼至少在之後寫新功能時,記得加上測試。測試寫得好很差,甚至比功能自己更能看出編程水平。
測試可讓代碼更增強壯。項目沒出bug時,皆大歡喜,有沒有測試都同樣;一旦出現難纏的bug,你就會無比想念一套完善的測試代碼了。
博主寫本身的網站時就沒有對測試給與足夠的重視,回想起來走了不少彎路。但願讀者之前車之鑑,培養良好的編程習慣。