Code without tests is brokenk as designed —— Jacob Kaplan Moss
没有测试的代码设计上是有缺陷的。
软件开发过程中测试的意义:
- 短反馈周期,团队可以快速调整
- 减少debug上的时间
- 测试实际上起到了代码说明文档的作用
- 重构代码后,测试可以确保你是否破坏原有功能
- 测试可以防止你的发际线后移
最好的测试方法是测试驱动开发(TDD),一下是TDD的步骤:
- 写测试用例:测试充实功能
- 运行测试用例:这次肯定会失败,因为还没有写代码
- 写代码:好让测试用例通过
- 运行测试:如果所有的测试用例通过了,你可以自信你已经完成了测试所需要求
- 重构代码:消除重复、解耦、封装、去除复杂性、增强可读性,每做一次重构都重新运行一次测试
- 重复上述步骤:去开发新功能
一下为使用TDD方法构建一个bucketlist API,提供CRUD和认证功能。
Bucketlist
API应该包含一下功能:
- 创建
- 获取
- 修改
- 删除
其他的需求包括:
- 认证
- 搜索
- 分页
- 等其他功能
Django Rest Framework(以下简称DRF)
创建项目目录:mkdir projects && $_
创建虚拟环境(使用python3):virtualenv venv -p python3
激活虚拟环境(Linux/Mac):source venv/bin/activate
安装依赖:pip install django djangorestframework
保存依赖:pip freeze >> requirements.txt
创建django项目:django-admin startproject djangorest
将DRF添加至django项目
打开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
]
创建 Rest API 应用
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
]
编写测试用例
打开/api/tests.py
,添加如下内容:
(本次测试目的:验证可创建一个bucketlist对象,并可保存进数据库)
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)
然后,准备好一个模型类,打开/api/models.py
from django.db import models
class Bucketlist(models.Model):
pass
运行测试:python manage.py test
此次测试,屏幕上应该会出现一大串错误信息,可以不用管,因为还没开始编写代码。
补充/api/models.py
内容如下:
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)
做好数据库的迁移:
python3 manage.py makemigrations
python3 manage.py migrate
再次运行测试:python manage.py test
屏幕应该会出现如下结果(本次只运行一次,应该是`Run 1 test in 0.002s):
Serializers(序列化)
新建/api/serializers.py
,并编写内容如下:
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
可以自动通过模型的域生成相应的serializer class,可以减少大量编码工作。
准备视图文件
首先在/api/tests.py
里添加如下测试用例:
# 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)
测试:可通过视图文件创建一个bucketlist。
运行测试,本次运行会出错,接下来编辑/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,创建/api/urls.py
,添加如下内容:
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
里定义的路由添加进项目路由(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
]
本次运行测试会通过了。
开启服务器:python manage.py runserver
打开`http://127.0.0.1:8000/bucketlists
继续编写读取、更新、删除操作
在api/tests.py
里更新测试用例:
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
里补充代码:
class DetailsView(generics.RetrieveUpdateDestroyAPIView):
"""This class handles the http GET, PUT and DELETE requests."""
queryset = Bucketlist.objects.all()
serializer_class = BucketlistSerializer
在api/urls.py
里补充路由
from .views import DetailsView
url(r'^bucketlists/(?P<pk>[0-9]+)/$',
DetailsView.as_view(), name="details"),