本文是学习了Python的 unittest 和 Django的自动化测试 之后的整理总结。
Django的单元测试使用了Python的
unittest
模块,这个模块使用基于类的方式定义测试。Django创建测试用例的类
django.test.TestCase
继承自Python的
unittest.TestCase
。下图为Django官网给到的类的继承关系图:
在计算机编程中,单元测试是一种软件测试的方法,通过单元测试对源代码的各个单元的源码进行测试,确定它们是否适合使用。这里的单元(unit)指的是一个或者多个计算机程序模块的集合,包含相关的控制数据、使用程序和操作程序。
test fixture (测试夹具) :为了执行一个或者多个测试进行的准备和清除操作。
test case (测试用例) :一个单独的测试单元。检查一个特定输入的响应。
test suite (测试套件) :一系列测试用例。
test runner (测试运行器) :组织测试的执行,提供一个输出给用户。
本文通过编写 《Django入门》 中的demo项目的单元测试来进行练习。
示例-1 : 调用创建接口,接口返回状态码为201,并且测试数据库中新增了一条数据。
from django.test import TestCase, Client
from django.urls import reverse
from todo.models import Todo
class TodoTestCase(TestCase):
def setUp(self):
todo_data = {
'content': '粉刷',
'remark': '我是一个粉刷匠',
'priority': 5
Todo.objects.create(**todo_data)
def test_create_todo(self):
'''测试待办事项的创建'''
todo_data = {
'content': '喝茶',
'remark': '粉刷累了喝杯茶',
'priority': 3
url = reverse('todo:create')
client = Client()
response = client.post(url, todo_data)
status_code = response.status_code
self.assertEqual(status_code, 201)
todo_queryset = Todo.objects.all()
self.assertEqual(todo_queryset.count(), 2)
setUp() 和 tearDown() 方法允许你定义在每一个测试方法被执行之前(setUp
)和之后(tearDown
)要执行的内容。
reverse()获取路径模式对应的url。
Django断言方法
Python的unittest
模块有以下断言方法:
方法 检查内容 assertEqual(a, b) a == b
assertNotEqual(a, b) a != b
assertTrue(x) bool(x) is True
assertFalse(x) bool(x) is False
assertIs(a, b) a is b
assertIsNot(a, b) a is not b
assertIsNone(x) x is None
assertIsNotNone(x) x is not None
assertIn(a, b) a in b
assertNotIn(a, b) a not in b
assertIsInstance(a, b) isinstance(a, b)
assertNotIsInstance(a, b) not isinstance(a, b)
Django的断言在此基础上新增了一些有用的断言,以下是其中的几个:
1.SimpleTestCase.assertRaisesMessage(expected_exception,expected_message)
:
断言引发了expected_exception
异常(比如ValueError
),并且异常的消息为expected_message
。
2.SimpleTestCase.assertJSONEqual(raw, expected_data, msg=None)
:
断言JSON片段row
和excepted_data
相等。如果不相等,会输出错误信息,以及msg
的内容。
3.SimpleTestCase.assertJSONNotEqual(raw, expected_data, msg=None)
:
断言JSON片段row
和excepted_data
不相等。
模拟接口请求
Django提供了一个测试客户端(即Client类),作为一个虚拟的Web浏览器。可以用它来模拟接口请求,测试项目中的视图(View
)。
from django.test import Client
client = Client()
response = client.post('/todo/create/', {'content': '喝茶', 'remark': '粉刷累了喝杯茶', 'priority': 3})
status_code = response.status_code # 201
get请求
get(path, data=None, follow=False, secure=False, **extra)
path
是请求路径。
data
是一个键值对字典,是请求携带的数据。
extra
关键字参数用于指定请求头的内容。
follow
为True
时会跟随任何重定向,并在响应对象中设置redirect_chain
属性。
secure
为True
会模拟HTTPS请求。
c = Client()
c.get('/some/api/', {'title': '标题', 'content': '内容'}, HTTP_ACCEPT='application/json')
通过extra
传递的内容需要遵循 CGI (Common Gateway Interface)规范,传递给CGI程序的环境变量以HTTP_
开头的时候,会设置对应的HTTP请求头的值,比如HTTP_ACCEPT='application/json'
会设置请求头字段ACCEPT
为'application/json'
。
post请求
post(path, data=None,content_type=MULTIPART_CONTENT, follow=False, secure=False, **extra)
content_type
为application/json
的时候,data
为字典、列表或者元组时,会使用json.dumps()
进行序列化。
如果设置其他的content_type
,使用content_type
作为请求头的Content-Type
的内容,data
不做处理。
如果不设置content_type
的值,data
会以multipart/form-data
的内容类型传输。
测试文件上传的话,可以这样写:
with open(path, 'rb') as file:
url = reverse('some_url_pattern_name')
file_name = 'some_file_name'
data = {
'file': file,
'file_name': file_name,
c = Client()
res = c.post(url, data)
其他请求的模拟和get
、post
类似:
1. head(path, data=None, follow=False, secure=False, **extra)
2. options(path, data='', content_type='application/octet-stream', follow=False, secure=False, **extra)
3. put(path, data='', content_type='application/octet-stream', follow=False, secure=False, **extra)
4. patch(path, data='', content_type='application/octet-stream', follow=False, secure=False, **extra)
5. delete(path, data='', content_type='application/octet-stream', follow=False, secure=False, **extra)
6. trace(path, follow=False, secure=False, **extra)
测试数据库
如果测试依赖数据库访问,比如创建/查询模块,必须要使用django.test.TestCase 而不是unittest.TestCase。需要数据库的测试不会使用“真正的”数据库,它会为测试单独创建一个数据库,然后在测试被执行完之后销毁该数据库。
在 示例1 中,在测试前使用代码创建了一条数据:
class TodoTestCase(TestCase):
def setUp(self):
todo_data = {
'content': '粉刷',
'remark': '我是一个粉刷匠',
'priority': 5
Todo.objects.create(**todo_data)
如果想要在测试之前,在数据库中添加很多数据,可以使用夹具(fixture)。一个夹具是一个包含数据库序列化内容的文件集合,Django知道怎么将它们导入数据库中。
夹具导入数据在setUp
之前发生。
TransactionTestCase.fixtures提供了数据库夹具。这样使用:
class SomeTestCase(TestCase):
fixtures = ['data1.json', 'data2']
这会用loaddata加载fixtures
中的文件中的数据到测试数据库中,文件的后缀名需要是序列化器(serializer )的名称(xml
, json
, jsonl
, yaml
)。如果没有使用文件后缀,就会查找所有序列化类型的后缀名的文件,比如data2
会查找data2.xml
, data2.json
, data2.jsonl
, data2.yaml
。
可以使用dumpdata从数据库中的数据中生成夹具文件,语法如下:
django-admin dumpdata [app_label[.ModelName] [app_label[.ModelName] ...]]
示例-2:
1.使用dumpdata
从现有数据库中的数据生成一个json
文件:
python3 manage.py dumpdata todo.Todo --output todo/tests/todo.json
生成的todo.json
文件内容如下:
[{"model": "todo.todo", "pk": 1, "fields": {"content": "第一件事就是写文1", "remark": "写文使人快乐", "priority": 3, "completed": false, "created_time": "2021-10-05T12:25:59.514Z", "modified_time": "2021-10-05T12:25:59.514Z"}}]
2.写测试用例
class TodoTestCaseOne(TestCase):
fixtures = ['todo/tests/fixtures/todo.json']
def test_fixtures(self):
'''测试夹具导入的数据'''
todo_queryset = Todo.objects.all()
self.assertEqual(todo_queryset.count(), 1)
这里的todo_queryset
拿到的数据是之前通过夹具导入数据库的。
3.运行测试用例
使用python3 manage.py test
执行测试。
Mock(模拟)
unittest.mock提供了:
Mock类,用于创建模拟对象。
patch()装饰器,用于方便地模拟被测试模块中的类或者对象。
Mock类
unittest.mock.Mock
类创建一个Mock
对象,它包含一些可选的参数来指定Mock对象的行为:
spec:可以是一个字符串列表或者一个作为模拟对象说明的对象(一个类或者一个实例)。如果传递一个对象,那么会对该对象调用dir函数,产生一个对象的属性的字符串列表,访问任何不在此列表中的属性会抛出 AttributeError。
spec_set:一个spec
的更严格的变体。如果使用了spec_set
,当尝试在模拟对象上设置或获取一个spec_set
中不存在的属性时,会抛出AttributeError
。
side_effect:mock
被调用时 被调用的函数。 side_effect。
return_value:mock
被调用时 的返回值。 return_value 。
unsafe:默认访问任何以assert, assret,asert, aseert,assrt
开头的属性名时,会抛出 AttributeError
。unsafe=True
会允许对这些属性的访问。
wraps:mock
对象包裹的项。如果wraps
不为None
,调用mock
时会传递调用到被包裹的对象中。
name:mock
的名称。
>>> mock = Mock(side_effect=KeyError('foo'))
>>> mock()
Traceback (most recent call last):
KeyError: 'foo'
patch 装饰器
patch()
模拟的位置不是在哪里定义的路径,是在哪里使用的路径。假设我们想测试的项目有以下的结构:
-> Defines SomeClass
-> from a import SomeClass
-> some_function instantiates SomeClass
在a.py
文件中,定义了某个类:SomeClass
,在b.py
中导入了这个类,在函数some_function
中实例化了SomeClass
,现在要使用patch()
模拟SomeClass
,使用的是:
@patch('b.SomeClass')
但如果使用的是import a
,some_function
使用的是a.SomeClass
,则要使用:
@patch('a.SomeClass')
示例-3:
1.准备三个文件,c.py
文件调用了b.py
的函数,b.py
调用了a.py
的文件,在获取待办事项列表接口中调用c.py
中的函数。
a.py
:
def a_function():
return True
b.py
:
from a import a_function
def b_function():
a_result = a_function()
if a_result:
return 'a_function返回的结果为真'
else:
raise Exception('a_function返回的结果为假')
b
文件中,当a_function
函数返回值为假时,会抛出一个异常,最终导致接口请求返回400的错误(其实服务端代码的异常不应该返回400状态码,这里仅仅练习用)。
c.py
:
from b import b_function
def c_function():
b_function()
views.py
:
from .models import Todo
from .utils.c import c_function
class TodoListView(View):
def get(self, request, pk):
'''获取清单列表'''
try:
c_function()
todo_list = Todo.objects.all()
2.测试获取待办事项列表接口时,模拟这个情况下a_function
的返回值:
class TodoTestCaseTwo(TestCase):
def setUp(self):
todo_data = {
'content': '粉刷',
'remark': '我是一个粉刷匠',
'priority': 5
Todo.objects.create(**todo_data)
@patch('todo.utils.b.a_function', return_value=True)
def test_get_todo_a_true(self, mock_a):
'''测试获取待办事件列表
模拟的a_function的返回值为True
todo_queryset = Todo.objects.all() # 这里能拿到数据
print(todo_queryset.count(), 'todo_queryset.count()\n')
url = reverse('todo:list')
client = Client()
response = client.get(url) # 但是这里View内部的代码拿到的todo表的数据为空
status_code = response.status_code
self.assertEqual(status_code, 200)
@patch('todo.utils.b.a_function', return_value=False)
def test_get_todo_a_false(self, mock_a):
'''测试获取待办事件列表
模拟的a_function的返回值为False
url = reverse('todo:list')
client = Client()
response = client.get(url)
status_code = response.status_code
self.assertEqual(status_code, 400)
运行单元测试
使用manage.py
应用程序的 test
指令运行测试:
python3 manage.py test
默认情况下,会查找当前工作目录下的所有test*.py
(文件名以test
开头)文件中的 所有测试用例(测试用例是unittest.TestCase
的子类),自动在测试用例外创建一个测试套件,然后运行该测试套件。
在测试用例中以test
开头的方法,代表测试方法。