频道栏目
首页 > 程序开发 > web前端 > HTML/CSS > 正文
《Web接口开发与自动化测试基于Python语言》--第6章
2017-06-29 10:10:00         来源:zhaoxz1985的博客  
收藏   我要投稿

读书笔记">《Web接口开发与自动化测试基于Python语言》–读书笔记

第6章 Django测试

 

这章来到本书的正题了。

Web应用的难点在于: HTTP层面的请求处理、表单验证和处理、模板渲染;

Django框架的测试模块解决的问题: 模拟请求、插入测试数据、检查应用输出。

 

6.1 unittest单元测试框架

6.1.1 单元测试框架

误区:

不用单元测试框架一样可以编写单元测试,单元测试本质上就是通过一段代码去测试另一段代码;

单元测试框架不仅可以用于程序单元级别的测试,同样可以用于UI自动化测试、接口自动化测试,以及移动APP自动化测试。

单元测试框架:

提供用例编写规范与执行: 单元测试框架提供了统一的用例编写规范,灵活指定不同级别的测试,如针对一个测试方法、一个测试类、一个测试文件,或者一个测试目录等不同级别的测试。

提供专业的比较方法: 测试用例最关键的步骤,实际测试结果与预期结果的比较,单元测试将这个比较过程命名为“断言”,单元测试框架提供了丰富的断言方法,eg:相等/不相等,包含/不包含,True/False等。

提供丰富的测试日志: 单元测试框架提供了丰富的执行日志,当测试用例执行失败的时候会抛出明确的失败信息,测试完成后提供结果信息,失败用例数、成功用例数、执行时间等。

单元测试框架可帮助我们完成不同级别测试的自动化:

单元测试:unittest

HTTP接口自动化测试:unittest+Requests

Web UI自动化测试:unittest+Selenium

移动自动化测试:unittest+Appium

 

6.1.2 编写单元测试用例

简单示例:

对两个整数的简单计算module.py:

#! /usr/bin python
# -*- coding:utf-8 -*-

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 p(self):
        return self.a / self.b

编写对应的测试文件test.py:

#! /usr/bin python
# -*- coding:utf-8 -*-

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_p(self):
        result = self.cal.p()
        self.assertEqual(result, 2)

if __name__ == "__main__":
    # unittest.main()
    # 构造测试集
    suite = unittest.TestSuite()
    suite.addTest(ModuleTest("test_add"))
    suite.addTest(ModuleTest("test_mul"))
    suite.addTest(ModuleTest("test_sub"))
    suite.addTest(ModuleTest("test_p"))
    # 执行测试
    runner = unittest.TextTestRunner()
    runner.run(suite)

通过unittest单元测试框架编写的测试用例,更加规范和整洁。

对代码进行解释:

首先,import导入unittest单元测试框架;

其次,创建ModuleTest类继承unittest.TestCase类;

setUp()方法,用于测试用例执行前的初始化工作,eg:初始化变量、生成数据库测试数据、打开浏览器等;

tearDown()方法,用于测试用例执行之后的善后工作,eg:清除数据库测试数据、关闭文件、关闭浏览器等;

然后,创建具体的测试用例,包含被测试数据、预期测试结果;

接下来,调用unittest.TestSuite()类的addTest()方法,向测试套件中添加测试用例,所谓测试套件可以理解为测试用例的集合;

最后,通过unittest.TextTestRunner()类的run()方法运行测试套件中的测试用例。

注意:

根据unittest单元测试框架的要求,测试用例必须以“test”开头,eg:test_add、test_mul;

如果想默认运行当前测试文件中的所有测试用例,可以使用:unittest.main()方法。

测试结果如下:

from django.test import TestCase
from sign.models import Guest, Event

# 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="2016-08-31 02:18:22")
        Guest.objects.create(id=1, event_id=1, realname="alen", phone='13711001101',email="alen@mail.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)

对上述代码进行分析:

首先,还是创建ModelTest类继承django.test.TestCase测试类;

然后,setUp()方法,初始化针对发布会表和嘉宾表的测试数据;

最后,通过test_event_models()、test_guest_models()测试方法,分别查询创建的数据,并对返回结果进行断言是否符合预期;

注意:

千万不要单独执行tests.py文件,Django专门提供了test命令来运行测试,效果如下:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'HOST': '127.0.0.1',
        'PORT': '3306',
        'NAME': 'guest',
        'USER': 'root',
        'PASSWORD': 'nsfocus',
        #'OPTIONS': {
        #    'init_command': "SET sql_mode='STRICT_TRANS_TABLES'",
        #},
    }
}

然后修改MySQL数据库的配置文件:/etc/mysql/mysql.conf.d/mysqld.cnf,增加配置:

sql_mode = ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION
第二个警告就简单了,只需要将提示的内容直接从配置里去掉即可。

6.2.2 运行测试用例

test命令,提供了可以控制测试用例执行的级别。

运行sign应用下的所有测试用例:

#! /usr/bin python
# -*- coding:utf-8 -*-

from django.test import TestCase


# Create your tests here.
# 测试sign应用的视图
class IndexPageTest(TestCase):

    def test_index_page_renders_index_template(self):
        '''测试index视图'''
        response = self.client.get('/index/')             # 虽然没有导入django.test.Client类,但是self.client最终调用的依然是django.test.Client类的方法,请求/index/路径
        self.assertEqual(response.status_code, 200)       # status_code获取HTTP返回的状态码,使用assertEqual断言状态码是否为200
        self.assertTemplateUsed(response, 'index.html')   # 使用assertTemplateUsed()断言服务器是否使用的是index.html模板进行响应

6.3.2 测试登录动作

继续使用上面的方法对首页的登录动作进行测试,修改/guest/sign/tests.py:

class LoginActionTest(TestCase):
    '''测试登录动作'''

    def setUp(self):                                     # 初始化,调用User.objects.create_user创建登录用户数据
        User.objects.create_user('admin', 'admin@mail.com', 'admin123456')

    def test_add_admin(self):
        '''测试添加的用户数据是否正确'''
        user = User.objects.get(username='admin')
        self.assertEqual(user.username, 'admin')
        self.assertEqual(user.email, 'admin@mail.com')    # 注意这里书中有误,user表里的字段是email而不是mail,否则会报错

    def test_login_action_username_password_null(self):
        '''测试用户名密码为空'''
        test_data = {'username':'', 'password': ''}
        response = self.client.post('/login_action/', data=test_data)    # 通过post()方法请求'/login_aciton/'路径测试登录功能
        self.assertEqual(response.status_code, 200)
        self.assertIn(b'username or password error!', response.content)   # assertIn()方法断言返回的HTML页面中是否包含指定的提示字符串

    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, 302)    # 这里为什么断言的是302,是因为登录成功后,通过HttpResponseRedirect()跳转到了'/event_manage/'路径,这是一个重定向

6.3.3 测试发布会管理

继续使用上面的方法对发布会管理视图进行测试,修改/guest/sign/tests.py:

class EventManageTest(TestCase):
    """测试发布会管理"""

    def setUp(self):
        '''初始化测试数据,包括登录用户数据,发布会数据'''
        User.objects.create_user('admin', 'admin@mail.com', 'admin123456')
        Event.objects.create(name='xiaomi5', limit=2000, address='beijing', status=1, start_time='2017-08-10 12: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_search_success(self):
        '''测试发布会搜索'''
        # 这里自己给自己挖了个坑,post登录请求的时候少写了一个/,当时写成了'/login_action',我擦一执行测试就返回302,排查了好半天才发现,哎,需要认真仔细啊
        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 测试嘉宾管理

继续使用上面的方法对嘉宾管理视图进行测试,修改/guest/sign/tests.py:

class GuestManageTest(TestCase):
    """测试嘉宾管理"""

    def setUp(self):
        '''还是使用setUp初始化一些测试数据'''
        User.objects.create_user('admin', 'admin@mail.com', 'admin123456')
        Event.objects.create(id=1, name='xiaomi5', limit=2000, address='beijing', status=1, start_time='2017-08-10 12:30:00')
        Guest.objects.create(realname='alen', phone=18611001100, email='alen@mail.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_search_success(self):
        '''测试嘉宾搜索功能'''
        response = self.client.post('/login_action/', data=self.login_user)
        # 这里就是坑了,我们根据书中描述一步一步来得话,我们在views.py里定义的搜索功能是根据名字来搜索的,而不是根据手机号,下面应该修改为('/search_realname/', {'realname':'alen'})
        # response = self.client.post('/search_phone/', {'phone':'18611001100'})
        response = self.client.post('/search_realname/', {'realname':'alen'})
        self.assertEqual(response.status_code, 200)
        self.assertIn(b'alen', response.content)
        self.assertIn(b'18611001100', response.content)

上面的代码,虫师给大家挖了很多坑,如果只编写了测试代码而未进行实际测试,是不会发现有问题的,我已经备注了,大家参见上面的备注吧。

其他知识点没有什么,基本和上面的类似,都是setUp初始化测试数据,然后分别对两个视图函数进行测试。

6.3.5 测试用户签到

继续使用上面的方法对签到管理视图进行测试,修改/guest/sign/tests.py:

class SignIndexActionTest(TestCase):
    """测试发布会签到"""

    def setUp(self):
        User.objects.create_user('admin', 'admin@mail.com', 'admin123456')
        Event.objects.create(id=1, name="xiaomi5", limit=2000, address='beijing', status=1, start_time='2017-8-10 12:30:00')
        Event.objects.create(id=2, name="oneplus4", limit=2000, address='shenzhen', status=1, start_time='2017-6-10 12:30:00')
        Guest.objects.create(realname="alen", phone=18611001100, email='alen@mail.com', sign=0, event_id=1)
        Guest.objects.create(realname="una", phone=18611011101, email='una@mail.com', sign=1, event_id=2)
        self.login_user = {'username':'admin', 'password':'admin123456'}

    def test_event_models(self):
        '''测试添加的发布会数据'''
        result1 = Event.objects.get(name='xiaomi5')
        self.assertEqual(result1.address, 'beijing')
        self.assertTrue(result1.status)
        result2 = Event.objects.get(name='oneplus4')
        self.assertEqual(result2.address, 'shenzhen')
        self.assertTrue(result2.status)

    def test_guest_models(self):
        '''测试添加的嘉宾数据'''
        result = Guest.objects.get(realname='alen')
        self.assertEqual(result.phone, '18611001100')
        self.assertEqual(result.event_id, 1)
        self.assertFalse(result.sign)

    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":"18611011101"})
        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)

测试嘉宾签到功能只是在数据初始化构造上内容多了,测试的点覆盖了签到功能全部分支,都比较好理解,只是在最终执行测试结果的时候,我崩溃了,6个测试用例中出现了2个失败,详细失败原因见下面:

    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_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)

发现了一个共同点,这两个测试用例都是针对测试数据发布会id=1的,而与之关联的测试嘉宾的签到sign值在初始化的时候是sign=0,也就是未签到的状态。

将涉及到的两个值,分别做修改,如果我把这两个用例里的发布会id从1改为2,会发现再次执行的结果里“测试手机号为空”执行成功了,但是“测试嘉宾签到成功”依然还是失败的,捋一下,改成2,也就是对应的嘉宾已经签到了,所以“测试嘉宾签到成功”失败也是自然的。

如果我们把初始化数据里的嘉宾alen的sign改为1已签到,再次执行的时候,发现结果和上面一样,都是“测试手机号为空”能通过,但是“测试嘉宾签到成功”失败。

这里也真的是奇怪了,为什么会出现这种情况,我现在抛开测试数据,直接去看下真实的数据情况,因为之前为了验证嘉宾签到代码,已经全部都签到了,只能通过修改数据库的方式,将sign从1改为0,点击发布会页面的sign链接,发现的确会报404错误,奇怪了,为什么没嘉宾签到,就返回404错误呢?按理来说,即使没有嘉宾签到,从发布会点击签到页面,也应该展示签到页面,只不过显示的已签到数为0。

自己在这段时间里,跑偏了很久,想过是不是签到功能不完善,难道需要先将sign从初始化的0update为1,再执行测试用例?或者是提示404,找不到页面,那我就单独把sign_index_action的html从sign_index.html里独立出来?……

自己真的是跑偏了太久,休假前到休假后,中间隔了快一周时间,再次查看views.py里的签到功能代码才发现问题所在:

并不是虫师给大家挖坑,而是自己给自己挖了一个大坑!自己在做虫师的作业的时候,也就是在签到页面显示总的嘉宾数和已签到嘉宾数的时候,使用了一个擅自查资料使用的方法:get_list_or_404,一切的罪过都是由它而来,我们来看下面的代码就:

# 签到页面
@login_required
def sign_index(request, eid):
    username = request.session.get('user', '')
    event = get_object_or_404(Event, id=eid)
    guest_list = len(get_list_or_404(Guest, event_id=eid))
    guest_sign = len(get_list_or_404(Guest, event_id=eid, sign=1))
    return render(request, 'sign_index.html', {"user": username, "event": event, 'guest_list': guest_list, 'guest_sign': guest_sign})

这里使用的get_list_or_404()方法,当sign=1的时候并没有问题,因为始终都能获取到已经签到的嘉宾数量,但是一旦sign=0,就会导致get_list_or_404方法直接返回404错误,而不是返回0个已签到嘉宾,自己当时以为查找到了一个类似get_object_or_404相类似的好方法去获取嘉宾数量,但是没想到当获取不到嘉宾数量的时候应该怎样展示!聪明反被聪明误啊!

那么该如何去修改呢?其实很简单,只要正常去查询数据库,获取到嘉宾总数和已签到嘉宾数量就可以了,修改后的代码如下:

# 签到页面
@login_required
def sign_index(request, eid):
    username = request.session.get('user', '')
    event = get_object_or_404(Event, id=eid)
    guest_list = len(Guest.objects.filter(event_id=eid))
    guest_sign = len(Guest.objects.filter(event_id=eid, sign=1))
    return render(request, 'sign_index.html', {"user": username, "event": event, 'guest_list': guest_list, 'guest_sign': guest_sign})

修复后,再去执行测试用例,就全部通过了。

6.4 总结

至此,本章关于Django测试的内容就结束了,总结起来,就是如下几点:

Django的test库,提供了丰富的单元测试方法;

test库中的TestCase方法可以测试模型、视图;

每个测试用例必须以test命名开头;

一些断言关键字如:assertEqual判断是否相等、assertFalse判断为否、assertTrue判断为是、assertIn判断包含;

基本的测试套路就是构造数据,对指定测试内容传递数据进行测试,对返回结果进行判断。

更多关于Django测试方法技巧请参见官方文档:

Django测试部分官方文档

点击复制链接 与好友分享!回本站首页
上一篇:jQuery中的proxy()和delegation()方法
下一篇:vue2+element管理后台集成解决方案
相关文章
图文推荐
点击排行

关于我们 | 联系我们 | 广告服务 | 投资合作 | 版权申明 | 在线帮助 | 网站地图 | 作品发布 | Vip技术培训 | 举报中心

版权所有: 红黑联盟--致力于做实用的IT技术学习网站