使用 Django 進(jìn)行測(cè)試驅(qū)動(dòng)開(kāi)發(fā)
所謂測(cè)試驅(qū)動(dòng)開(kāi)發(fā)(TDD),就是先編寫(xiě)測(cè)試用例,然后編寫(xiě)代碼來(lái)滿足測(cè)試用例,具體包含以下步驟:
編寫(xiě)測(cè)試用例。 編寫(xiě)代碼滿足測(cè)試用例中的需求。 運(yùn)行測(cè)試用例。 如果通過(guò),說(shuō)明代碼滿足了測(cè)試用例所定義的需求。 如果未通過(guò),則需要重構(gòu)代碼,直到通過(guò)。 重復(fù)以上步驟,直到通過(guò)全部的測(cè)試用例。
通常情況下,我們都是先寫(xiě)代碼,然后編寫(xiě)測(cè)試用例,因此測(cè)試驅(qū)動(dòng)開(kāi)發(fā)是反直覺(jué)的,那為什么還要這么做呢?基于以下幾點(diǎn)原因:
TDD 可以被認(rèn)為是根據(jù)測(cè)試用例來(lái)說(shuō)明需求。此后編寫(xiě)源代碼,重點(diǎn)是滿足這些要求。當(dāng)測(cè)試最終通過(guò)時(shí),你可以確信已滿足要求。這種專注可以幫助開(kāi)發(fā)人員避免范圍蔓延。 TDD 可以通過(guò)較短的開(kāi)發(fā)周期提高開(kāi)發(fā)效率。一次解決測(cè)試用例中的個(gè)別可以最大限度地減少干擾因素。重大更改將更容易跟蹤和解決。減少了調(diào)試工作,提高了效率,并且將更多時(shí)間花在開(kāi)發(fā)上。 編寫(xiě)測(cè)試時(shí)考慮到了需求。正因?yàn)槿绱?,它們更有可能被?xiě)成明確的,可以理解的。這樣的測(cè)試可以作為代碼庫(kù)的優(yōu)質(zhì)文檔。 先編寫(xiě)測(cè)試用例可確保您的源代碼始終具有可測(cè)試性,它還保證隨著代碼庫(kù)的增長(zhǎng),測(cè)試覆蓋率始終保持在合理的百分比。
然而,測(cè)試驅(qū)動(dòng)開(kāi)發(fā)也不是銀彈,以下情形并不適合測(cè)試驅(qū)動(dòng)開(kāi)發(fā):
當(dāng)需求不明確時(shí),有時(shí)續(xù)期會(huì)隨著開(kāi)發(fā)的進(jìn)行而逐漸明確,在這種情況下最初編寫(xiě)的任何測(cè)試可能會(huì)過(guò)時(shí)。 開(kāi)發(fā)的目的是為了證明某一概念時(shí)——例如在黑客馬拉松期間,測(cè)試通常不是優(yōu)先事項(xiàng)。
了解了測(cè)試驅(qū)動(dòng)開(kāi)發(fā)之后,我們用 Django 來(lái)演示一下測(cè)試驅(qū)動(dòng)開(kāi)發(fā)的過(guò)程。(Python 3.7 以上,Django 2.0 以上)
首先描述需求,我們要實(shí)現(xiàn)這樣一個(gè)單位換算功能的 Web 應(yīng)用,可以在厘米、米、英里直接互相轉(zhuǎn)換,Web 界面如圖所示:


1、創(chuàng)建項(xiàng)目
完整代碼公眾號(hào)回復(fù)【TDD】獲取。
首先,我們創(chuàng)建一個(gè)名字叫 convert 的項(xiàng)目:
pip?install?django
django-admin?startproject?converter
此時(shí) Django 已經(jīng)為我們生成了 converter 目錄及基本的項(xiàng)目文件:
converter/
????converter/
????????__init__.py
????????settings.py
????????urls.py
????????wsgi.py
????manage.py
然后,進(jìn)入 converter 目錄,創(chuàng)建一個(gè)名字叫 length 的 app:
cd?converter
python?manage.py?startapp?length
然后你會(huì)看到這樣的目錄結(jié)構(gòu):
converter/
????converter/
????????__init__.py
????????settings.py
????????urls.py
????????wsgi.py
????length/
????????__init__.py
????????admin.py
????????apps.py
????????migrations/
????????????__init__.py
????????models.py
????????tests.py
????????views.py
????manage.py
2、配置 app
修改 converter/settings.py,在 INSTALLED_APPS 里加入 lengh :
INSTALLED_APPS?=?[
????.
????.
????.
????'length',
]
然后在 length 目錄下新建 urls.py,寫(xiě)入以下內(nèi)容:
from?django.urls?import?path
from?length?import?views
app_name?=?'length'
urlpatterns?=?[
????path('convert/',?views.convert,?name='convert'),
]
最后在 converter/urls.py 中指向 length/urls.py:
from?django.contrib?import?admin
from?django.urls?import?path,?include
urlpatterns?=?[
????path('admin/',?admin.site.urls),
????path('length/',?include('length.urls')),
]
這樣一個(gè)沒(méi)有任何業(yè)務(wù)邏輯的項(xiàng)目就創(chuàng)建成功了,接下來(lái)編寫(xiě)測(cè)試用例:
3、編寫(xiě)測(cè)試用例
在 lengh 目錄下新建 tests.py,寫(xiě)入以下內(nèi)容:
from?django.test?import?TestCase,?Client
from?django.urls?import?reverse
class?TestLengthConversion(TestCase):
????"""
????This?class?contains?tests?that?convert?measurements?from?one
????unit?of?measurement?to?another.
????"""
????def?setUp(self):
????????"""
????????This?method?runs?before?the?execution?of?each?test?case.
????????"""
????????self.client?=?Client()
????????self.url?=?reverse("length:convert")
????def?test_centimetre_to_metre_conversion(self):
????????"""
????????Tests?conversion?of?centimetre?measurements?to?metre.
????????"""
????????data?=?{
????????????"input_unit":?"centimetre",
????????????"output_unit":?"metre",
????????????"input_value":?8096.894
????????}
????????response?=?self.client.get(self.url,?data)
????????self.assertContains(response,?80.96894)
????def?test_centimetre_to_mile_conversion(self):
????????data?=?{
????????????"input_unit":?"centimetre",
????????????"output_unit":?"mile",
????????????"input_value":?round(985805791.3527409,?3)
????????}
????????response?=?self.client.get(self.url,?data)
????????self.assertContains(response,?6125.5113)
上述代碼有兩個(gè)測(cè)試用例,分別代表兩個(gè)需求。test_centimetre_to_metre_conversion 代表厘米轉(zhuǎn)米的需求,而 test_centimetre_to_mile_conversion 代表厘米轉(zhuǎn)英里的需求。
4、編寫(xiě)代碼
這和 Django 開(kāi)發(fā)沒(méi)什么兩樣,先編寫(xiě)一個(gè) forms.py,內(nèi)容如下:
from?django?import?forms
class?LengthConverterForm(forms.Form):
????MEASUREMENTS?=?(
????????('centimetre',?'厘米'),
????????('metre',?'米'),
????????('mile',?'英里')
????)
????input_unit?=?forms.ChoiceField(choices=MEASUREMENTS)
????input_value?=?forms.DecimalField(decimal_places=3)
????output_unit?=?forms.ChoiceField(choices=MEASUREMENTS)
????output_value?=?forms.DecimalField(decimal_places=3,?required=False)
然后編寫(xiě) html,在 length 目錄下新建 ?templates/length.html,內(nèi)容如下:
<html?lang="en">
??<head>
????<title>Length?Conversiontitle>
??head>
??<body>
????<form?action={%?url?"length:convert"?%}?method="get">
??????<div>
????????{{?form.input_unit?}}
????????{{?form.input_value?}}
??????div>
??????<input?type="submit"?value="轉(zhuǎn)換為:"/>
??????<div>
????????{{?form.output_unit?}}
????????{{?form.output_value?}}
??????div>
???form>
??body>
html>
然后編寫(xiě)最重要的視圖函數(shù) views.py,內(nèi)容如下:
from?django.shortcuts?import?render
from?length.forms?import?LengthConverterForm
convert_to_metre?=?{
????"centimetre":?0.01,
????"metre":?1.0,
????"mile":?1609.34
}
convert_from_metre?=?{
????"centimetre":?100,
????"metre":?1.0,
????"mile":?0.000621371
}
#?Create?your?views?here.
def?convert(request):
????form?=?LengthConverterForm()
????if?request.GET:
????????input_unit?=?request.GET['input_unit']
????????input_value?=?request.GET['input_value']
????????output_unit?=?request.GET['output_unit']
????????metres?=?convert_to_metre[input_unit]?*?float(input_value)
????????print(f"{metres?=?},?{input_value?=?}")
????????output_value?=?metres?*?convert_from_metre[output_unit]
????????data?=?{
????????????"input_unit":?input_unit,
????????????"input_value":?input_value,
????????????"output_unit":?output_unit,
????????????"output_value":?round(output_value,5)
????????}
????????form?=?LengthConverterForm(initial=data)
????????return?render(
????????????request,?"length.html",?context={"form":?form})
????return?render(
????????request,?"length.html",?context={"form":?form})
5、執(zhí)行測(cè)試
執(zhí)行測(cè)試并不需要啟動(dòng) django 的 runserver:

出現(xiàn) OK 說(shuō)明測(cè)試通過(guò),啟動(dòng) django:
python?manage.py?runserver
打開(kāi)瀏覽器,訪問(wèn) http://localhost:8000/length/convert/ 即可看到界面:

完整代碼公眾號(hào)回復(fù)【TDD】獲取。
最后的話
本文分享了什么是測(cè)試驅(qū)動(dòng)開(kāi)發(fā),并用測(cè)試驅(qū)動(dòng)開(kāi)發(fā)的方式創(chuàng)建了一個(gè)簡(jiǎn)單的 Django 應(yīng)用程序,用于長(zhǎng)度轉(zhuǎn)換。這和一般開(kāi)發(fā)的區(qū)別就是先寫(xiě)好測(cè)試用例,其他沒(méi)啥區(qū)別,這樣的方式可以使得需求更明確,開(kāi)發(fā)周期更短,增量可控,提高開(kāi)發(fā)效率,保證測(cè)試覆蓋率。
如果覺(jué)得有用,還請(qǐng)點(diǎn)贊、在看、關(guān)注支持,感謝!
