第6章 Django测试

内容来源于《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='[email protected]',
                              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', '[email protected]', 'admin123456')

    def test_add_admin(self):
        '''测试添加用户'''
        user = User.objects.get(username="admin")
        self.assertEqual(user.username, "admin")
        self.assertEqual(user.email, "[email protected]")

    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', '[email protected]', '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', '[email protected]', '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='[email protected]', 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', '[email protected]', '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='[email protected]', sign=0, event_id=1)
        Guest.objects.create(realname="una", phone=18611001101, email='[email protected]', 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,都通过了。


你可能感兴趣的:(第6章 Django测试)