更新时间:2020-08-07 来源:黑马程序员 浏览量:
在 Django 项目中,我们开发完一些功能模块之后,通常需要去写单元测试来检测代码的 bug。Django 框架内部提供比较方便的单元测试工具,接下来我们主要来学习如何写 Django 的单元测试,以及测试 Django 视图函数的方式和原理浅析。
环境准备
新建项目和应用
$ # 新建 django_example 项目 $ django-admin startproject django_example $ # 进入 django_example $ cd django_example $ # 新建 users 应用 $ ./manage.py startapp users 更新 django_example 项目的配置文件,添加 users 应用添加到 INSTALLED_APPS 中,关闭 csrf 中间件。 INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'users' ] MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', # 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ]
项目目录结构如下:
django_example ├── django_example │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── manage.py └── users ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ └── __init__.py ├── models.py ├── tests.py └── views.py
将 users/views.py 改为如下内容
import json from django.contrib.auth import login, authenticate, logout from django.shortcuts import render from django.views import View from django.http.response import JsonResponse class UserView(View): def get(self, request): if not request.user.is_authenticated: return JsonResponse({ 'code': 401, 'message': '用户未登录' }) return JsonResponse({ 'code': 200, 'message': 'OK', 'data': { 'username': request.user.username, } }) class SessionView(View): def post(self, request): """用户登录""" # 客户端的请求体是 json 格式 content_type = request.headers.get('Content-Type', '') if 'application/json' in content_type: data = json.loads(request.body) else: return JsonResponse({ 'code': 400, 'message': '非 json 格式' }) data = json.loads(request.body) username = data.get('username', '') password = data.get('password', '') user = authenticate(username=username, password=password) # 检查用户是否存在 if not user: return JsonResponse({ 'code': 400, 'message': '用户名或密码错误' }) # 执行登录 login(request, user) return JsonResponse({ 'code': 201, 'message': 'OK' }) def delete(self, request): """退出登录""" logout(request) return JsonResponse({ 'code': 204, 'message': 'OK' }) 在 django_example/urls.py 绑定接口 from django.contrib import admin from django.urls import path from users.views import UserView, SessionView urlpatterns = [ path('admin/', admin.site.urls), path('users', UserView.as_view()), path('session', SessionView.as_view()) ] 初始化数据库 $ ./manage.py makemigrations $ ./manage.py migrate
Django 单元测试介绍
上面的环境准备中我们写了 2 个类视图,SessionView 提供了用户登录、退出接口,UserView 提供了获取用户信息接口。接下来我们主要来看如何针对这些接口写单元测试。
在哪儿里写单元测试
Django中每一个应用下面都会有一个 tests.py 文件,我们将当前应用测试代码写在这个文件中。如果测试的代码量比较多,我们需要将测试的代码分模块,那么可以在当前应用下创建 tests 包。
单元测试代码如何写
django 提供了 django.test.TestCase 单元测试基础类,它继承自 python 标准库中 unittest.TestCase 。
我们通常定义类继承自 django.test.TestCase ,在类中我们定义 test_ 开头的方法,在方法中写具体的测试逻辑,一个类中可以包含多个 测试方法。
2个特殊的方法:
·def setUp(self) 这个方法会在每一个测试方法执行之前被调用,通常用来做一些准备工作
·def tearDown(self) 这个方法会在每一个测试用法执行之后被被调用,通常用来做一些清理工作
2 个特殊的类方法
@classmethod def setUpClass(cls) # 这个方法用于做类级别的准备工作,他会在测试执行之前被调用,且一个类中,只被调用一次 @classmthod def tearDownClass(cls): # 这个方法用于做类级别的准备工作,他会在测试执行结束后被调用,且一个类中,只被调用一次
Django 还是提供了 django.test.client.Client 客户端类,用于模拟客户端发起 [get|post|delete...] 请求,并且能够自动保存 cookie。
Client 还包含了 login 方法方便进行用户登录。
通过 client 发起请求的时候 url 是路径,不需要 schema://domain 这个前缀
如何执行单元测试
./manage.py test
如果想值测试具体的 app,或者 app 下的某个测试文件,测试类,测试方法,也是可以的,命令参数如下([] 表示可选):
./manage.py test [app_name][.test_file_name][.class_name][.test_method_name]
测试代码
users/tests.py
from django.test import TestCase from django.test.client import Client from django.contrib.auth.models import User class UserTestCase(TestCase): def setUp(self): # 创建测试用户 self.username = 'zhangsan' self.password = 'zhangsan12345' self.user = User.objects.create_user( username=self.username, password=self.password) # 实例化 client 对象 self.client = Client() # 登录 self.client.login(username=self.username, password=self.password) def tearDown(self): # 删除测试用户 self.user.delete() def test_user(self): """测试获取用户信息接口""" path = '/users' resp = self.client.get(path) result = resp.json() self.assertEqual(result['code'], 200, result['message']) class SessionTestCase(TestCase): @classmethod def setUpClass(cls): # 创建测试用户 cls.username = 'lisi' cls.password = 'lisi' cls.user = User.objects.create_user( username=cls.username, password=cls.password) # 实例化 client 对象 cls.client = Client() @classmethod def tearDownClass(cls): # 删除测试用户 cls.user.delete() def test_login(self): """测试登录接口""" path = '/session' auth_data = { 'username': self.username, 'password': self.password } # 这里我们设置请求体格式为 json resp = self.client.post(path, data=auth_data, content_type='application/json') # 将相应体转化为python 字典 result = resp.json() # 检查登录结果 self.assertEqual(result['code'], 201, result['message']) def test_logout(self): """测试退出接口""" path = '/session' resp = self.client.delete(path) result = resp.json() self.assertEqual(result['code'], 204, result['message']) 测试结果 $ ./manage.py test Creating test database for alias 'default'... System check identified no issues (0 silenced). ... ---------------------------------------------------------------------- Ran 3 tests in 0.309s OK Destroying test database for alias 'default'...
测试视图函数的方式
上面的代码是我们测试视图函数最简便的方式,我们是通过 client 对象模拟请求,该请求最终会路由到视图函数,并调用视图函数。
下面我们看看不通过 client,在测试方法中直接调用视图函数。
利用 RequestFactory 直接调用视图函数
大家知道每个视图函数都有一个固定参数 request,这个参数是客户端请求对象。如果我们需要直接测试视图函数,那么必须模拟这个请求对象,然后传递给视图函数。
django 提供了模拟请求对象的类 `django.test.client.RequestFactory` 我们通过 RequestFactory 对象的` [get|post|delete|...]` 方法来模拟请求对象,将该对象传递给视图函数,来实现视图函数的直接调用测试。
演示代码:
class SessionRequestFactoryTestCase(TestCase): @classmethod def setUpClass(cls): # 创建测试用户 cls.username = 'wangwu' cls.password = 'wangwu1234' cls.user = User.objects.create_user( username=cls.username, password=cls.password) @classmethod def tearDownClass(cls): # 删除测试用户 cls.user.delete() def test_login(self): """测试登录视图函数""" # 实例化 RequestFactory request_factory = RequestFactory() path = '/session' auth_data = { 'username': self.username, 'password': self.password } # 构建请求对象 request = request_factory.post(path, data=auth_data, content_type='application/json') # 登录的视图函数 login_funciton = SessionView().post # 调用视图函数 resp = login_funciton(request) # 打印视图函数返回的响应对象的 content,也就是响应体 print(resp.content) def test_logout(self): """测试退出视图函数""" # 实例化 RequestFactory request_factory = RequestFactory() path = '/session' request = request_factory.delete(path) # 退出的视图函数 logout_funciton = SessionView().delete # 调用视图函数 resp = logout_funciton(request) # 打印视图函数返回的响应对象的 content,也就是响应体 print(resp.content)
如果此时我们执行测试的话,会抛出异常信息 AttributeError: 'WSGIRequest' object has no attribute 'session' 。
原因分析
session 视图函数 get,post 会调用login 和 logout 函数,我们来看下这两个函数的源码
def login(request, user, backend=None): """ Persist a user id and a backend in the request. This way a user doesn't have to reauthenticate on every request. Note that data set during the anonymous session is retained when the user logs in. """ session_auth_hash = '' if user is None: user = request.user if hasattr(user, 'get_session_auth_hash'): session_auth_hash = user.get_session_auth_hash() if SESSION_KEY in request.session: if _get_user_session_key(request) != user.pk or ( session_auth_hash and not constant_time_compare(request.session.get(HASH_SESSION_KEY, ''), session_auth_hash)): # To avoid reusing another user's session, create a new, empty # session if the existing session corresponds to a different # authenticated user. request.session.flush() else: request.session.cycle_key() try: backend = backend or user.backend except AttributeError: backends = _get_backends(return_tuples=True) if len(backends) == 1: _, backend = backends[0] else: raise ValueError( 'You have multiple authentication backends configured and ' 'therefore must provide the `backend` argument or set the ' '`backend` attribute on the user.' ) else: if not isinstance(backend, str): raise TypeError('backend must be a dotted import path string (got %r).' % backend) request.session[SESSION_KEY] = user._meta.pk.value_to_string(user) request.session[BACKEND_SESSION_KEY] = backend request.session[HASH_SESSION_KEY] = session_auth_hash if hasattr(request, 'user'): request.user = user rotate_token(request) user_logged_in.send(sender=user.__class__, request=request, user=user) def logout(request): # ..... # remember language choice saved to session language = request.session.get(LANGUAGE_SESSION_KEY) request.session.flush() # ...... 从代码中我们可以看出这两个方法中需要对 request 对象的 session 属性进行相关操作。 而 django 中 session 是通过 django.contrib.sessions.middleware.SessionMiddleware 这个中间件来完成,源码如下: class SessionMiddleware(MiddlewareMixin): def __init__(self, get_response=None): self.get_response = get_response engine = import_module(settings.SESSION_ENGINE) self.SessionStore = engine.SessionStore def process_request(self, request): session_key = request.COOKIES.get(settings.SESSION_COOKIE_NAME) # 设置了 session 属性 request.session = self.SessionStore(session_key) # ......
我们可以看出 session 属性是在 SessionMiddleware.process_request 中设置的。
我们通过 RequestFactory 只是创建了请求对象,没有被中间件处理过,所以也就请求对象中也就没有了 session 属性。
解决办法
既然某些请求对象需要经过中间件处理,那么我们是否可以手动调用中间件处理一下呢?答案是肯定的。我们在调用视图函数前先让中间件处理一下请求对象。
from django.contrib.sessions.middleware import SessionMiddleware # ..... def test_login(self): """测试登录视图函数""" # 实例化 RequestFactory request_factory = RequestFactory() path = '/session' auth_data = { 'username': self.username, 'password': self.password } # 构建请求对象 request = request_factory.post(path, data=auth_data, content_type='application/json') # 调用中间件处理 session_middleware = SessionMiddleware() session_middleware.process_request(request) # 登录的视图函数 login_funciton = SessionView().post # 调用视图函数 resp = login_funciton(request) # 打印视图函数返回的响应对象的 content,也就是响应体 print(resp.content) def test_logout(self): """测试退出视图函数""" # 实例化 RequestFactory request_factory = RequestFactory() path = '/session' request = request_factory.delete(path) # 调用中间件处理 session_middleware = SessionMiddleware() session_middleware.process_request(request) # 退出的视图函数 logout_funciton = SessionView().delete # 调用视图函数 resp = logout_funciton(request) # 打印视图函数返回的响应对象的 content,也就是响应体 print(resp.content)
总结
我们通过 RequestFactory 模拟的请求对象,然后传递给视图函数,来完成视图函数的直接调用测试,如果需要经过中间件的处理,我们需要手动调用中间件。
django 视图函数测试的两种方法对比和原理浅析
django 请求的处理流程的大致如下: 创建 request 对象-->执行中间层处理-->视图函数处理-->中间层处理-->返回乡响应对象。
我们可以从源码中看出来:
class WSGIHandler(base.BaseHandler): request_class = WSGIRequest def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # 初始化时,加载中间件 self.load_middleware() def __call__(self, environ, start_response):# 按照 WSGI 协议接受参数 set_script_prefix(get_script_name(environ)) signals.request_started.send(sender=self.__class__, environ=environ) # 创建请求对象,默认是 WSGIRequest 对象 request = self.request_class(environ) # 获取响应对象,get_response 执行 中间件-->视图函数-->中间件 response = self.get_response(request) response._handler_class = self.__class__ status = '%d %s' % (response.status_code, response.reason_phrase) response_headers = [ *response.items(), *(('Set-Cookie', c.output(header='')) for c in response.cookies.values()), ] # 按照 wsgi 协议返回数据 start_response(status, response_headers) if getattr(response, 'file_to_stream', None) is not None and environ.get('wsgi.file_wrapper'): response = environ['wsgi.file_wrapper'](response.file_to_stream) return response
我们来看下 client 的核心源码:
# django/test/client.py class ClientHandler(BaseHandler): def __call__(self, environ): # 加载中间件 if self._middleware_chain is None: self.load_middleware() # ... # 构建 WSGIRequest 请求对象 request = WSGIRequest(environ) # 调用中间件-->视图函数-->中间件 response = self.get_response(request) # .... # 返回响应对象 return response class Client(RequestFactory): def request(self, **request): # ... # 模拟 wsgi 协议的 environ 参数 environ = self._base_environ(**request) # .... try: # 调用 ClientHandler response = self.handler(environ) except TemplateDoesNotExist as e: # .... # .... # 给响应对象添加额外的属性和方法,例如 json 方法 response.client = self response.request = request # Add any rendered template detail to the response. response.templates = data.get("templates", []) response.context = data.get("context") response.json = partial(self._parse_json, response) return response 我们来看下 RequestFactory 的核心源码 # django/test/client.py class RequestFactory: def _base_environ(self, **request): """ The base environment for a request. """ # This is a minimal valid WSGI environ dictionary, plus: # - HTTP_COOKIE: for cookie support, # - REMOTE_ADDR: often useful, see #8551. # See https://www.python.org/dev/peps/pep-3333/#environ-variables return { 'HTTP_COOKIE': '; '.join(sorted( '%s=%s' % (morsel.key, morsel.coded_value) for morsel in self.cookies.values() )), 'PATH_INFO': '/', 'REMOTE_ADDR': '127.0.0.1', 'REQUEST_METHOD': 'GET', 'SCRIPT_NAME': '', 'SERVER_NAME': 'testserver', 'SERVER_PORT': '80', 'SERVER_PROTOCOL': 'HTTP/1.1', 'wsgi.version': (1, 0), 'wsgi.url_scheme': 'http', 'wsgi.input': FakePayload(b''), 'wsgi.errors': self.errors, 'wsgi.multiprocess': True, 'wsgi.multithread': False, 'wsgi.run_once': False, **self.defaults, **request, } def request(self, **request): "Construct a generic request object." # 这里只是返回了一个请求对象 return WSGIRequest(self._base_environ(**request))
从源码中大家可以看出 Client 集成自 RequestFactory 类。
Client 对象通过调用 request 方法来发起完整的请求: 创建 request 对象-->执行中间层处理-->视图函数处理-->中间层处理-->返回乡响应对象。
RequestFactory 对象的 request 方法只做了一件事 :创建 request 对象 ,所以我们要手动实现后面的完整过程。
总结
本章主要介绍了如何写 django 的单元测试,测试视图函数的两种方式和部分源码剖析,在实际工作中,我们通常使用 Client 来方便测试,遇到请求对象比较特殊或者执行流程复杂的时候,就需要通过 RequestFactory 这种方式。
猜你喜欢: