内容来源于《Web接口开发与自动化测试——基于Python语言》虫师编著,如有涉及版权问题,归虫师本人所有。请大家支持虫师的著作:http://www.broadview.com.cn/book/4811
源码下载:https://github.com/defnngj/guest
单元测试的好处:
- 当编写新代码的时候,你可以使用测试来验证你的代码是否像预期一样工作。
- 当重构或者修改旧代码的时候,你可以使用测试来确保你的修改不会影响到应用的运行。
6.1 unittest单元测试框架
Django默认使用Python的标准库unittest编写测试用例。
6.1.1 单元测试框架
有2个误区需要澄清:
- 误区1:不用单元测试框架一样可以编写单元测试,单元测试本质上就是通过一段代码去测试另外一段代码。
- 误区2:单元测试框架不仅可以用于程序单元级别的测试,同样可以用于UI自动化测试、接口自动化测试,以及移动APP自动化测试等。
单元测试框架提供了哪些功能:
- 提供用例编写规范与执行;
- 提供专业的比较方法;
- 提供丰富的测试日志
单元测试:unittest
HTTP接口自动化测试:unittest + Requests
Web UI自动化测试:unittest + Selenium
移动自动化测试:unittest + Appium
6.1.2 编写单元测试用例
写一个实现加减乘除的计算器功能。
class Calculator():
def __init__(self, a, b):
self.a = int(a)
self.b = int(b)
def add(self):
return self.a + self.b
def sub(self):
return self.a - self.b
def mul(self):
return self.a * self.b
def div(self):
return self.a / self.b
下面写测试代码
import unittest
from module import Calculator
class ModuleTest(unittest.TestCase):
def setUp(self):
self.cal = Calculator(8, 4)
def tearDown(self):
pass
def test_add(self):
result = self.cal.add()
self.assertEqual(result, 12)
def test_sub(self):
result = self.cal.sub()
self.assertEqual(result, 4)
def test_mul(self):
result = self.cal.mul()
self.assertEqual(result, 32)
def test_div(self):
result = self.cal.div()
self.assertEqual(result, 2)
if __name__ == "__main__":
# 构造测试集
suite = unittest.TestSuite()
suite.addTest(ModuleTest("test_add"))
suite.addTest(ModuleTest("test_sub"))
suite.addTest(ModuleTest("test_mul"))
suite.addTest(ModuleTest("test_div"))
# 执行测试
runner = unittest.TextTestRunner()
runner.run(suite)
首先,通过import导入unittest单元测试框架。创建ModuleTest类继承unittest.TestCase类。
setUp()和tearDown()分别在每一个测试用例的开始和结束时执行。
unittest要求测试用例(方法)必须以“test”开头。
接下来,调用unittest.TestSuite()类的addTest()方法向测试套件中添加测试用例。
最后,通过unittest.TextTestRunner()类的run()方法运行测试套件中的测试用例。
执行结果:
(venv) liujindeMacBook-Pro:ven2 liujin$ python3 test.py
....
----------------------------------------------------------------------
Ran 4 tests in 0.000s
OK
4个点表示运行通过4条用例。
6.2 Django测试
Django的单元测试类django.test.TestCase从unittest.TestCase继承而来。
6.2.1 一个简单的例子
from django.test import TestCase
from sign.models import Event, Guest
# Create your tests here.
class ModelTest(TestCase):
def setUp(self):
Event.objects.create(id=1, name="oneplus 3 event", status=True, limit=2000, address='shenzhen',
start_time='2018-08-31 14:18:22')
Guest.objects.create(id=1, event_id=1, realname='alen', phone='13711001101', email='alen@email.com',
sign=False)
def test_event_models(self):
result = Event.objects.get(name="oneplus 3 event")
self.assertEqual(result.address, "shenzhen")
self.assertTrue(result.status)
def test_guest_models(self):
result = Guest.objects.get(phone='13711001101')
self.assertEqual(result.realname, "alen")
self.assertFalse(result.sign)
在setUp()初始化方法中,分别创建一条发布会和嘉宾数据。最后,通过test_event_models()和test_guest_models()测试方法,分别查询创建的数据,并断言数据是否正确。
(venv) liujindeMacBook-Pro:guest liujin$ python3 manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..
----------------------------------------------------------------------
Ran 2 tests in 0.013s
OK
Destroying test database for alias 'default'...
2条都通过了。当Django在执行setUp()操作时,并不会真正地向数据库插入数据。所以,不必关心产生测试数据之后的清理工作。
把"shenzhen"改成"beijing",使测试执行失败。
(venv) liujindeMacBook-Pro:guest liujin$ python3 manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
F.
======================================================================
FAIL: test_event_models (sign.tests.ModelTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/liujin/Documents/virtualenv3.7Demo/venv/bin/guest/sign/tests.py", line 15, in test_event_models
self.assertEqual(result.address, "beijing")
AssertionError: 'shenzhen' != 'beijing'
- shenzhen
+ beijing
----------------------------------------------------------------------
Ran 2 tests in 0.015s
FAILED (failures=1)
Destroying test database for alias 'default'...
从测试执行信息中,很容易找到错误的原因。
6.2.2 运行测试用例
运行sign应用下的所有测试用例。
python3 manage.py test sign
运行sign应用下的tests.py测试文件。
python3 manage.py test sign.tests
运行sign应用test.py测试文件下的ModelTest测试类。
python3 manage.py test sign.tests.ModelTest
执行ModelTest测试类下面的test_event_models测试方法。
python3 manage.py test sign.tests.ModelTest.test_event_models
使用-p参数模糊匹配测试文件
python3 manage.py test -p test*.py
6.3 客户端测试
在Django中,django.test.Client类充当一个虚拟的网络浏览器,可以测试视图与Django的应用程序以编程方式交互。
(venv) liujindeMacBook-Pro:guest liujin$ python3 manage.py shell
Python 3.7.0 (v3.7.0:1bf9cc5093, Jun 26 2018, 23:26:24)
[Clang 6.0 (clang-600.0.57)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from django.test.utils import setup_test_environment
>>> setup_test_environment()
setup_test_environment()用来测试前初始化测试环境。
>>> from django.test import Client
>>> c = Client()
>>> response = c.get('/index/')
>>> response.status_code
200
状态码200表示请求成功。
6.3.1 测试首页
打开tests.py文件,编写index视图的测试用例。
class IndexPageTest(TestCase):
'''测试index登录首页'''
def test_index_page_renders_index_template(self):
'''测试index视图'''
response = self.client.get('/index/')
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'index.html')
6.3.2 测试登录动作
接下来在tests.py中编写登录动作的测试用例。
......
from django.contrib.auth.models import User
......
class LoginActionTest(TestCase):
'''测试登录动作'''
def setUp(self):
User.objects.create_user('admin', 'admin@email.com', 'admin123456')
def test_add_admin(self):
'''测试添加用户'''
user = User.objects.get(username="admin")
self.assertEqual(user.username, "admin")
self.assertEqual(user.email, "admin@email.com")
def test_login_action_username_password_null(self):
'''用户名密码为空'''
test_data = {'username': '', 'password': ''}
response = self.client.post('/login_action/', data=test_data)
self.assertEqual(response.status_code, 200)
self.assertIn(b"username or password error!", response.content)
def test_login_action_username_password_error(self):
'''用户名密码错误'''
test_data = {'username': 'abc', 'password': '123'}
response = self.client.post('/login_action/', data=test_data)
self.assertEqual(response.status_code, 200)
self.assertIn(b"username or password error!", response.content)
def test_login_action_success(self):
'''登录成功'''
test_data = {'username': 'admin', 'password': 'admin123456'}
response = self.client.post('/login_action/', data=test_data)
self.assertEqual(response.status_code, 200)
- 在setUp()初始化方法中,创建登录用户数据。
- test_add_admin()用于测试添加的用户数据是否正确。
- test_login_action_success()用例测试用户名和密码正确。为什么断言HTTP返回状态码是302而不是200呢?这是因为在login_action视图函数中,当用户登录验证成功后,通过HttpResponseRedirect()跳转到"/event_manage/"路径,这是一个重定向,所以登录成功的HTTP返回码是302。见下面:
(venv) liujindeMacBook-Pro:guest liujin$ python3 manage.py test sign.tests.LoginActionTest
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.F..
======================================================================
FAIL: test_login_action_success (sign.tests.LoginActionTest)
登录成功
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/liujin/Documents/virtualenv3.7Demo/venv/bin/guest/sign/tests.py", line 65, in test_login_action_success
self.assertEqual(response.status_code, 200)
AssertionError: 302 != 200
----------------------------------------------------------------------
Ran 4 tests in 0.924s
FAILED (failures=1)
Destroying test database for alias 'default'...
6.3.3 测试发布会管理
接下来在tests.py中编写发布会管理视图的测试用例。
......
from sign.models import Event
......
class EventManageTest(TestCase):
'''发布会管理'''
def setUp(self):
User.objects.create_user('admin', 'admin@email.com', 'admin123456')
Event.objects.create(name="xiaomi5", limit=2000, address='beijing', status=1, start_time='2018-08-10 14:30:00')
self.login_user = {'username': 'admin', 'password': 'admin123456'}
def test_event_manage_success(self):
'''测试发布会xiaomi5'''
response = self.client.post('/login_action/', data=self.login_user)
response = self.client.post('/event_manage/')
self.assertEqual(response.status_code, 200)
self.assertIn(b"xiaomi5", response.content)
self.assertIn(b"beijing", response.content)
def test_event_manage_sreach_success(self):
'''测试发布会搜索'''
response = self.client.post('/login_action/', data=self.login_user)
response = self.client.post('/search_name/', {"name": "xiaomi5"})
self.assertEqual(response.status_code, 200)
self.assertIn(b"xiaomi5", response.content)
self.assertIn(b"beijing", response.content)
因为发布会管理event_manage和发布会搜索search_name这两个视图函数被@login_required修饰,所以想测试这两个功能,必须要先登录,并且要构造登录用户数据。所以你看到在每个用例的开始先调用登录函数。
6.3.4 测试嘉宾管理
继续在tests.py中编写嘉宾管理的测试用例。
class GuestManageTest(TestCase):
'''嘉宾管理'''
def setUp(self):
User.objects.create_user('admin', 'admin@email.com', 'admin123456')
Event.objects.create(id=1, name="xiaomi5", limit=2000, address='beijing', status=1, start_time='2018-08-10 12:30:00')
Guest.objects.create(realname="alen", phone=18611001100, email='alen@email.com', sign=0, event_id=1)
self.login_user = {'username': 'admin', 'password': 'admin123456'}
def test_event_manage_success(self):
'''测试嘉宾信息alen'''
response = self.client.post('/login_action/', data=self.login_user)
response = self.client.post('/guest_manage/')
self.assertEqual(response.status_code, 200)
self.assertIn(b"alen", response.content)
self.assertIn(b"18611001100", response.content)
def test_guest_manage_sreach_success(self):
'''测试嘉宾搜索'''
response = self.client.post('/login_action/', data=self.login_user)
response = self.client.post('/guest_search/', {"phone": "18611001100"})
self.assertEqual(response.status_code, 200)
self.assertIn(b"alen", response.content)
self.assertIn(b"18611001100", response.content)
嘉宾管理guest_manage 和嘉宾搜索guest_search的测试需要构造完整的数据。首先是登录用户的数据,其次是嘉宾所属的某场发布会数据。
6.3.5 测试用户签到
最后写签到的测试用例。
class SignIndexActionTest(TestCase):
'''发布会签到'''
def setUp(self):
User.objects.create_user('admin', 'admin@email.com', 'admin123456')
Event.objects.create(id=1, name="xiaomi5", limit=2000, address="beijing", status=1, start_time='2018-08-10 12:30:00')
Event.objects.create(id=2, name="oneplus5", limit=2000, address="shenzhen", status=1, start_time='2018-09-10 14:00:00')
Guest.objects.create(realname="alen", phone=18611001100, email='alen@eamil.com', sign=0, event_id=1)
Guest.objects.create(realname="una", phone=18611001101, email='una@email.com', sign=1, event_id=2)
self.login_user = {'username': 'admin', 'password': 'admin123456'}
def test_sign_index_action_phone_null(self):
'''手机号为空'''
response = self.client.post('/login_action/', data=self.login_user)
response = self.client.post('/sign_index_action/1/', {"phone": ""})
self.assertEqual(response.status_code, 200)
self.assertIn(b"phone error.", response.content)
def test_sign_index_action_phone_or_event_id_error(self):
'''手机号或发布会id错误'''
response = self.client.post('/login_action/', data=self.login_user)
response = self.client.post('/sign_index_action/2/', {"phone": "18611001100"})
self.assertEqual(response.status_code, 200)
self.assertIn(b"event id or phone error.", response.content)
def test_sign_index_action_user_sign_has(self):
'''用户已签到'''
response = self.client.post('/login_action/', data=self.login_user)
response = self.client.post('/sign_index_action/2/', {"phone": "18611001101"})
self.assertEqual(response.status_code, 200)
self.assertIn(b"user has sign in.", response.content)
def test_sign_index_action_sign_success(self):
'''签到成功'''
response = self.client.post('/login_action/', data=self.login_user)
response = self.client.post('/sign_index_action/1/', {"phone": "18611001100"})
self.assertEqual(response.status_code, 200)
self.assertIn(b"sign in success!", response.content)
关于签到,测试验证的情况比较多,在setUp()中构造测试数据时需要创建两条发布会信息,两条嘉宾信息分别属于两个发布会,并且一个是已签到,一个是未签到。
跑一下测试用例:
(venv) liujindeMacBook-Pro:guest liujin$ python3 manage.py test sign.tests.SignIndexActionTest
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.18611001100
.18611001100
.18611001101
.
----------------------------------------------------------------------
Ran 4 tests in 1.119s
OK
Destroying test database for alias 'default'...
OK,都通过了。