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):
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
打開`http://127.0.0.1:8000/bucketlists
繼續(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"),