Django 測(cè)試驅(qū)動(dòng)開發(fā)案例(一)

Code without tests is brokenk as designed —— Jacob Kaplan Moss
沒有測(cè)試的代碼設(shè)計(jì)上是有缺陷的。

軟件開發(fā)過程中測(cè)試的意義:

  • 短反饋周期刨秆,團(tuán)隊(duì)可以快速調(diào)整
  • 減少debug上的時(shí)間
  • 測(cè)試實(shí)際上起到了代碼說(shuō)明文檔的作用
  • 重構(gòu)代碼后杆逗,測(cè)試可以確保你是否破壞原有功能
  • 測(cè)試可以防止你的發(fā)際線后移

最好的測(cè)試方法是測(cè)試驅(qū)動(dòng)開發(fā)(TDD)悔常,一下是TDD的步驟:

  • 寫測(cè)試用例:測(cè)試充實(shí)功能
  • 運(yùn)行測(cè)試用例:這次肯定會(huì)失敗港柜,因?yàn)檫€沒有寫代碼
  • 寫代碼:好讓測(cè)試用例通過
  • 運(yùn)行測(cè)試:如果所有的測(cè)試用例通過了,你可以自信你已經(jīng)完成了測(cè)試所需要求
  • 重構(gòu)代碼:消除重復(fù)乞封、解耦鲫惶、封裝蜈首、去除復(fù)雜性、增強(qiáng)可讀性,每做一次重構(gòu)都重新運(yùn)行一次測(cè)試
  • 重復(fù)上述步驟:去開發(fā)新功能

一下為使用TDD方法構(gòu)建一個(gè)bucketlist API欢策,提供CRUD和認(rèn)證功能吆寨。

Bucketlist

API應(yīng)該包含一下功能:

  • 創(chuàng)建
  • 獲取
  • 修改
  • 刪除

其他的需求包括:

  • 認(rèn)證
  • 搜索
  • 分頁(yè)
  • 等其他功能

Django Rest Framework(以下簡(jiǎn)稱DRF)

創(chuàng)建項(xiàng)目目錄:mkdir projects && $_
創(chuàng)建虛擬環(huán)境(使用python3):virtualenv venv -p python3
激活虛擬環(huán)境(Linux/Mac):source venv/bin/activate
安裝依賴:pip install django djangorestframework
保存依賴:pip freeze >> requirements.txt
創(chuàng)建django項(xiàng)目:django-admin startproject djangorest

將DRF添加至django項(xiàng)目

打開djangoerst/settings.py,在INSTALLED_APPS里添加如下行:

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles', # Ensure a comma ends this line
    'rest_framework', # Add this line
]

創(chuàng)建 Rest API 應(yīng)用

python manage.py startapp api

打開djangoerst/settings.py踩寇,在INSTALLED_APPS里添加如下行:

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'api', # Add this line
]

編寫測(cè)試用例

打開/api/tests.py啄清,添加如下內(nèi)容:
(本次測(cè)試目的:驗(yàn)證可創(chuàng)建一個(gè)bucketlist對(duì)象,并可保存進(jìn)數(shù)據(jù)庫(kù))

from django.test import TestCase
from .models import Bucketlist

class ModelTestCase(TestCase):
    """This class defines the test suite for the bucketlist model."""

    def setUp(self):
        """Define the test client and other test variables."""
        self.bucketlist_name = "Write world class code"
        self.bucketlist = Bucketlist(name=self.bucketlist_name)

    def test_model_can_create_a_bucketlist(self):
        """Test the bucketlist model can create a bucketlist."""
        old_count = Bucketlist.objects.count()
        self.bucketlist.save()
        new_count = Bucketlist.objects.count()
        self.assertNotEqual(old_count, new_count)

然后俺孙,準(zhǔn)備好一個(gè)模型類辣卒,打開/api/models.py

from django.db import models

class Bucketlist(models.Model):
    pass

運(yùn)行測(cè)試:python manage.py test

此次測(cè)試,屏幕上應(yīng)該會(huì)出現(xiàn)一大串錯(cuò)誤信息睛榄,可以不用管荣茫,因?yàn)檫€沒開始編寫代碼。

補(bǔ)充/api/models.py內(nèi)容如下:

from django.db import models

class Bucketlist(models.Model):
    """This class represents the bucketlist model."""
    name = models.CharField(max_length=255, blank=False, unique=True)
    date_created = models.DateTimeField(auto_now_add=True)
    date_modified = models.DateTimeField(auto_now=True)

    def __str__(self):
        """Return a human readable representation of the model instance."""
        return "{}".format(self.name)

做好數(shù)據(jù)庫(kù)的遷移:

python3 manage.py makemigrations
python3 manage.py migrate

再次運(yùn)行測(cè)試:python manage.py test

屏幕應(yīng)該會(huì)出現(xiàn)如下結(jié)果(本次只運(yùn)行一次场靴,應(yīng)該是`Run 1 test in 0.002s):


image.png

Serializers(序列化)

新建/api/serializers.py啡莉,并編寫內(nèi)容如下:

from rest_framework import serializers
from .models import Bucketlist

class BucketlistSerializer(serializers.ModelSerializer):
    """Serializer to map the Model instance into JSON format."""

    class Meta:
        """Meta class to map serializer's fields with the model fields."""
        model = Bucketlist
        fields = ('id', 'name', 'date_created', 'date_modified')
        read_only_fields = ('date_created', 'date_modified')

ModelSerializer可以自動(dòng)通過模型的域生成相應(yīng)的serializer class,可以減少大量編碼工作旨剥。

準(zhǔn)備視圖文件

首先在/api/tests.py里添加如下測(cè)試用例:

# Add these imports at the top
from rest_framework.test import APIClient
from rest_framework import status
from django.core.urlresolvers import reverse

# Define this after the ModelTestCase
class ViewTestCase(TestCase):
    """Test suite for the api views."""

    def setUp(self):
        """Define the test client and other test variables."""
        self.client = APIClient()
        self.bucketlist_data = {'name': 'Go to Ibiza'}
        self.response = self.client.post(
            reverse('create'),
            self.bucketlist_data,
            format="json")

    def test_api_can_create_a_bucketlist(self):
        """Test the api has bucket creation capability."""
        self.assertEqual(self.response.status_code, status.HTTP_201_CREATED)

測(cè)試:可通過視圖文件創(chuàng)建一個(gè)bucketlist咧欣。

運(yùn)行測(cè)試,本次運(yùn)行會(huì)出錯(cuò)轨帜,接下來(lái)編輯/api/views.py文件:

from rest_framework import generics
from .serializers import BucketlistSerializer
from .models import Bucketlist

class CreateView(generics.ListCreateAPIView):
    """This class defines the create behavior of our rest api."""
    queryset = Bucketlist.objects.all()
    serializer_class = BucketlistSerializer

    def perform_create(self, serializer):
        """Save the post data when creating a new bucketlist."""
        serializer.save()

處理url魄咕,創(chuàng)建/api/urls.py,添加如下內(nèi)容:

from django.conf.urls import url, include
from rest_framework.urlpatterns import format_suffix_patterns
from .views import CreateView

urlpatterns = {
    url(r'^bucketlists/$', CreateView.as_view(), name="create"),
}

urlpatterns = format_suffix_patterns(urlpatterns)

/api/urls.py里定義的路由添加進(jìn)項(xiàng)目路由(djangorest/urls.py):

from django.conf.urls import url, include

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^', include('api.urls')) # Add this line
]

本次運(yùn)行測(cè)試會(huì)通過了阵谚。

開啟服務(wù)器:python manage.py runserver

成功運(yùn)行服務(wù)器

打開`http://127.0.0.1:8000/bucketlists

image.png

繼續(xù)編寫讀取、更新烟具、刪除操作

api/tests.py里更新測(cè)試用例:

def test_api_can_get_a_bucketlist(self):
        """Test the api can get a given bucketlist."""
        bucketlist = Bucketlist.objects.get()
        response = self.client.get(
            reverse('details',
            kwargs={'pk': bucketlist.id}), format="json")

        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertContains(response, bucketlist)

    def test_api_can_update_bucketlist(self):
        """Test the api can update a given bucketlist."""
        change_bucketlist = {'name': 'Something new'}
        res = self.client.put(
            reverse('details', kwargs={'pk': bucketlist.id}),
            change_bucketlist, format='json'
        )
        self.assertEqual(res.status_code, status.HTTP_200_OK)

    def test_api_can_delete_bucketlist(self):
        """Test the api can delete a bucketlist."""
        bucketlist = Bucketlist.objects.get()
        response = self.client.delete(
            reverse('details', kwargs={'pk': bucketlist.id}),
            format='json',
            follow=True)

        self.assertEquals(response.status_code, status.HTTP_204_NO_CONTENT)

api/views.py里補(bǔ)充代碼:

class DetailsView(generics.RetrieveUpdateDestroyAPIView):
    """This class handles the http GET, PUT and DELETE requests."""

    queryset = Bucketlist.objects.all()
    serializer_class = BucketlistSerializer

api/urls.py里補(bǔ)充路由

from .views import DetailsView 

url(r'^bucketlists/(?P<pk>[0-9]+)/$',
        DetailsView.as_view(), name="details"),

文章來(lái)源

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末梢什,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子朝聋,更是在濱河造成了極大的恐慌嗡午,老刑警劉巖,帶你破解...
    沈念sama閱讀 210,914評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件冀痕,死亡現(xiàn)場(chǎng)離奇詭異荔睹,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)言蛇,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評(píng)論 2 383
  • 文/潘曉璐 我一進(jìn)店門僻他,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人腊尚,你說(shuō)我怎么就攤上這事吨拗。” “怎么了?”我有些...
    開封第一講書人閱讀 156,531評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵劝篷,是天一觀的道長(zhǎng)哨鸭。 經(jīng)常有香客問我,道長(zhǎng)娇妓,這世上最難降的妖魔是什么像鸡? 我笑而不...
    開封第一講書人閱讀 56,309評(píng)論 1 282
  • 正文 為了忘掉前任,我火速辦了婚禮哈恰,結(jié)果婚禮上只估,老公的妹妹穿的比我還像新娘。我一直安慰自己蕊蝗,他們只是感情好仅乓,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,381評(píng)論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蓬戚,像睡著了一般夸楣。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上子漩,一...
    開封第一講書人閱讀 49,730評(píng)論 1 289
  • 那天豫喧,我揣著相機(jī)與錄音,去河邊找鬼幢泼。 笑死紧显,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的缕棵。 我是一名探鬼主播孵班,決...
    沈念sama閱讀 38,882評(píng)論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼招驴!你這毒婦竟也來(lái)了篙程?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,643評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤别厘,失蹤者是張志新(化名)和其女友劉穎虱饿,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體触趴,經(jīng)...
    沈念sama閱讀 44,095評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡氮发,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,448評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了冗懦。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片爽冕。...
    茶點(diǎn)故事閱讀 38,566評(píng)論 1 339
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖披蕉,靈堂內(nèi)的尸體忽然破棺而出扇售,到底是詐尸還是另有隱情前塔,我是刑警寧澤,帶...
    沈念sama閱讀 34,253評(píng)論 4 328
  • 正文 年R本政府宣布承冰,位于F島的核電站华弓,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏困乒。R本人自食惡果不足惜寂屏,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,829評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望娜搂。 院中可真熱鬧迁霎,春花似錦、人聲如沸百宇。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)携御。三九已至昌粤,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間啄刹,已是汗流浹背涮坐。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留誓军,地道東北人袱讹。 一個(gè)月前我還...
    沈念sama閱讀 46,248評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像昵时,于是被迫代替她去往敵國(guó)和親捷雕。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,440評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容