添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
  • 扯淡,计算机基础 | 测试基础 | 测试过程 | 常用测试方法 | 缺陷管理 敏捷开发 | 软件质量管理 | 软件需求管理 | 浅谈测试模式 | 测试覆盖率 | 集成测试 | 系统测试
  • 因为当局者迷,旁观者清的道理,软件开发是个复杂而周期性的过程,期间很容易产生或遗留下错误,而对于开发人员自己所编写与开发的应用程序(软件),往往有很多问题是他们自己发现不了,所以如果直接把存在不足的、有错误、有漏洞的应用程序直接运营上线提供给用户使用,那么很可能会给企业带来商业风险或影响企业受益,所以就需要软件测试人员进行软件测试了。

    而软件测试(Software Testing)就是为了尽快尽早地发现软件的各种软件缺陷而展开的贯穿整个软件生命周期、对软件(包括阶段性产品)进行验证和确认的活动过程。这个过程是在规定的条件下对程序进行测试操作并对其是否能满足设计要求进行评估,以达到发现、纠正程序错误,衡量和提升软件质量的目的。通俗点说,软件测试就是通过各种各样的手段或工具来尽可能的找到软件的不足和错误。

    软件测试只能查找出软件中的错误或不足,但不能证明程序中没有错误,而且软件测试不能完全消灭软件的错误,只能尽早尽量多的发现软件中的错误与不足。

    软件生命周期是指从软件产品的可行性分析到软件不再使用而结束的时间。如果把软件看成是有生命的事物,那么软件的生命周期可分为6个阶段:需求分析、计划、设计、编码开发、测试、运行维护
    

    详情xmind参考:https://www.processon.com/mindmap/638c3ccc254ef949c5cf53d0

    软件测试从不同的角度有着不同的分类方式。

    在实际开发中,往往我们都是根据实际情况采用多种不同的测试手段、测试方式来对软件测试测试的。

    软件缺陷,通常又被叫做bug或者defect,即为软件或程序中存在的某种破坏正常运行能力的问题、错误,其存在的最终表现为用户所需要的功能没有完全实现,不能满足或不能全部满足用户的需求。

    产品内部来说,软件缺陷是软件产品开发或维护过程中所存在的错误、误差等各种问题。

    产品外部来说,软件缺陷是系统所需要实现的某种功能的没有或不足。

    bug出现的原因一般有如下几种情况,也就是说符合以下情况的问题都属于bug:

    缺陷管理也叫bug管理,一般会集成到项目管理工具中,常用的项目管理工具:Teambition禅道pingcode、飞书、钉钉等。大部分的项目管理工具内置的缺陷管理功能都会对缺陷划分成不同类型、严重等级、优先级别,以及不同的状态。

    bug类型 已指派(Assigned) 当bug被指认为New之后,将其传递给开发组,开发组将确认这是否是bug,如果是则开发组的leader会将bug指派给某位开发人员处理,并将bug的状态
    设定为“Assigned”。 重新指派(Reassigned) bug被重新指派给某位开发人员处理处理。 已打开(Open) 一旦开发人员开始处理bug,就将bug的状态设为“Open”。 已修复(Fixed) 当开发人员进行处理(并认为已经解决)之后,就可以将bug的状态设置为“Fixed”并将其提交给开发组leader,然后leader将bug返还给测试组。 等待再测试(Pending Reset) 当bug被返还到测试组后,会将bug的状态设置为“Pending Reset” 再测试(Reset) 测试组的leader将bug指定给某位测试人员进行再测试,并将bug的状态设置为“Reset”。 已关闭的(Closed) 如测试人员经过再次测试之后确认bug已被解决,会将bug的状态设置为 “Closed”。 再次打开的(Reopen) 如果经过再次测试发现bug仍然存在的话,测试人员将bug再次传递给开发组,并将bug的状态设置为“Reopen” 拒绝中(Pending Reject) 如果测试人员传递到开发组的bug被开发组认为不是bug时,这种情况下开发组可以拒绝,将bug的状态设置为“Pending Reject”并返还给测试组。 被拒绝的(Rejected) 测试组的负责人接到拒绝的bug时,如果发现并不能算作bug时,测试组负责人将bug的状态设置为“Rejected”。当然,无法重现,bug信息不足或重复的bug,有时候也会被拒绝。 延期(Postponed) 对于一些特殊的bug的测试需要搁置一段时间,这种情况下,bug的状态就被设置为“Postponed“。

    缺陷报告,也叫bug报告,是软件测试人员重要的产出物之一,也是主要工作之一。一份高质量的缺陷报告可以帮助开发人员快速定位问题,修复Bug;也便于测试人员对缺陷进行统计、分析和跟踪管理,是测试人员和开发人员重要的沟通工具。开发中针对需求,测试bug,最怕的就是口口相传。

    缺陷报告的基本组成:缺陷ID缺陷标题,发现者,前置条件,是否可重现,操作系统,发现时间,所属项目,所属模块,所属版本,缺陷状态,严重等级优先级别附件描述重现步骤,预期效果,实际效果等。注意:加粗部分为BUG六要素。

    参考模板:

    缺陷报告就是软件测试的结果产出物,而如何验证和测试缺陷?那就要继续往下学习更多内容了。

    测试显示软件存在缺陷 测试只能证明软件中存在缺陷,但并不能证明软件中不存在缺陷,即零缺陷是不可能的。
    软件测试是为了降低存在缺陷的可能性,即便是没有找到缺陷,也不能证明软件是完美的。 穷尽测试是不可能的 现在软件的规模越来越大,复杂度越来越高,想做到完全性的测试是不可能的。
    测试人员可以根据严重等级、优先级、场景、目的来分类别进行集中和高强度的测试,从而保证软件的质量。 测试尽早介入 测试人员越早介入软件开发流程越好,最好在需求阶段就开始介入,使缺陷在需求或设计阶段就被发现,
    缺陷发现越早,修复的成本就越小,反之,越晚发现修复成本就越高。 缺陷存在集群现象(二八定律) 80%的缺陷往往存在于20%的模块中。一般项目复杂功能往往会占据所有功能的20%左右,而这20%的复杂功能往往有可能会包含大部分的缺陷。一个功能模块发现的缺陷频率越高,那存在的未被发现的缺陷出现频率也越高,故发现的缺陷与未发现的缺陷成正比。开发团队里面最菜的人写的代码bug最多,开发团队里面负责开发功能最复杂难度最高的人bug最多。 杀虫剂悖论 反复使用相同的杀虫剂会导致害虫对杀虫剂产生免疫而无法杀死害虫,软件测试也一样。如果一直使用相同的测试方法或手段,可能无法发现新的bug。为了解决这个问题,测试用例应当定期修订和评审,增加新的或不同的测试用例帮助发现更多的缺陷。 测试依赖于环境 测试在不同环境(操作系统,浏览器,解释器)下是不同的。所以不应该以完全相同的⽅法去测试两个不同的系统。 不存在缺陷的谬论 与第一条类似,期望仅仅发现并修复⼤量缺陷就能确保系统的成功,这是⼀个谬论。

    测试自动化

    著名的敏捷开发布道师 Mike Cohn(迈克·科恩) 在他的着作《Succeeding with Agile》(中文名:《Scrum敏捷软件开发》)一书中提出了测试金字塔的概念。

    根据 Mike Cohn 的测试金字塔,测试的组合应该至少由以下三层组成 (自下往上分别是):

  • 单元测试(Unit Tests)
  • 服务测试(Services Tests)
  • 用户界面测试(UI Tests)
  • 意思是,应该把测试不同粒度的测试分布到整个软件不同层次中,而随着层次越高,编写的测试内容应该越少,也就是写许多小而快的低层次单元测试,适当写一些更粗粒度的中层次接口测试或集成测试,写很少的高层次UI测试、系统测试或验收测试。

    所以,根据测试金字塔理论,接下来我们按部就班对测试自动化的内容进行学习。

    禅道的介绍

    禅道项目管理软件(简称:禅道)集产品管理、项目管理、质量管理、文档管理、组织管理和事务管理于一体,是一款功能完备的项目管理软件,完美地覆盖了项目管理的核心流程。

    禅道的主要管理思想基于国际流行的敏捷项目管理方式—Scrum。Scrum是一种注重实效的敏捷项目管理方式,它规定了核心的管理框架 ,但具体的细节还需要团队自行扩充。禅道在遵循其管理方式基础上,又融入了国内研发现状的很多需求,比如bug管理,测试用例管理,发布管理,文档管理等。因此禅道不仅仅是一款scrum敏捷项目管理工具,更是一款完备的项目管理软件。基于scrum,又不局限于scrum。

    禅道最大的特色是创造性的将产品、项目、测试这三者的概念明确分开,互相配合,又互相制约。通过需求、任务、bug来进行交相互动,最终通过项目拿到合格的产品。

    目前,禅道和JIRA用的人较多。我们这里以禅道为例。

    禅道项目管理软件是做什么的?

    禅道由青岛易软天创网络科技有限公司开发,国产开源项目管理软件。它集产品管理、项目管理、质量管理、文档管理、组织管理和事务管理于一体,是一款专业的研发项目管理软件,完整覆盖了研发项目管理的核心流程。禅道管理思想注重实效,功能完备丰富,操作简洁高效,界面美观大方,搜索功能强大,统计报表丰富多样,软件架构合理,扩展灵活,有完善的API可以调用。禅道,专注研发项目管理!

    为什么用禅道这个名字?

    禅和道这两个字含义极其丰富,有宗教方面的含义,也有文化层面的含义。禅道项目管理软件取其文化含义,期望通过这两个字来传达我们对管理的理解和思考。这个名字是受《编程之道》和《编程之禅》这两本书的启发。英文里面的禅为Zen,道为Tao,所以我们软件的英文名字为zentao。

    Windows平台禅道的下载安装

    我们可以在Windows,Mac,linux平台去搭建禅道。

    下载地址:https://www.zentao.net/download/zentaopms12.2-80207.html

    如果是Windows平台,安装目录一定是磁盘的根目录

    Windows平台的默认账号密码:

    账号:admin
    密码:123456
    

    docker部署禅道

  • 新建一个容器卷挂载目录
  • [root@C /]# mkdir -p /docker_data/zento_data
    

    2.拉取镜像

    [root@C ~]# docker pull idoop/zentao:12.0.1
    
    docker run -d -p 6003:80  --restart=always -e ADMINER_USER="root" -e ADMINER_PASSWD="password" -e BIND_ADDRESS="false" -v /docker_data/zentao_data:/opt/zbox/ --add-host smtp.exmail.qq.com:163.177.90.125 --name zentao-server idoop/zentao:12.0.1
    
  • 现在就浏览器访问ip:6003端口即可,然后会让你修改密码,默认账号和密码:
  • 账号:admin
    密码:123456
    

    禅道使用流程

  • 开发经理(主管)
  • 开发团队1
  • 开发人员1
  • 开发人员2
  • 开发团队2
  • 开发人员1
  • 当项目创建后,需要设置项目团队(开发组/测试组),进行项目与产品及产品计划进行关联。注意,关联操作会自动的关联在产品计划中已激活的需求。也可以手动的关联需求。

    开发完成需求

    首先项目经理/项目主管/研发主管登录,项目 -- 需求列表中,可以为将需求进行分解,分成若干粒度更小的任务,便于开发进行开发工作。

    开发在任务列表中,能看到指派给自己的任务都有哪些,并且任务的时长。

    开发在开发该任务时,要每天填写工时,能及时查看任务进度。

    当该任务已经完成后,即剩余工时为0时,可以选择结束该任务,那此时的任务就处于已完成的状态。

    当所有的任务都完成后,并且开发自测通过后,就可以进行提测,在提测前,要进行构建版本的过程。

    开发人员登录,访问项目 --- 版本 -- 创建版本

    编写版本信息。

    在版本信息的描述中,要注意:

  • 该版本的实现哪些功能
  • 解决哪些问题?
  • 测试中,有些注意事项和建议
  • 版本构建成功后,如下图

    版本关联需求

    该版本实现了哪些需求

    赋予研发角色权限

    默认的,开发人员无权做给版本关联需求的操作,该权限只有研发主管、项目经理有权限。

    如何给开发赋予该权限:

  • 用admin账号登录,组织 -- 权限 --- 找到研发角色,点击权限维护按钮。
  • 下拉选择版本,勾选关联需求选项。
  • 此时,以开发人员的角色登录禅道,就可以发现已经有了该权限,也就是有了关联需求的按钮。
  • 注意,该操作适用于为所有的角色赋予指定权限。

    版本关联需求

    开发人员(研发主管/项目经理登录也行)登录,项目视图 ---- 版本选项,点击指定版本的关联需求按钮。

    勾(多)选需求,然后点击关联需求按钮。

    关联需求成功后,在版本详情中,能看到关联的需求:

    此时的项目,在当前产品计划中,开发阶段基本完成, 可以向测试提测。

    编写提测单:

    用户创建用例

    测试人员身份登录禅道,访问测试视图下的用例选项。

    编写测试用例。

    创建成功:

    版本关联用例

    测试视图 --- 测试单 --- 点击关联用例

    勾选用例并点击保存。

    关联成功:

    注意,只有用例的状态是正常的,才能被关联。

    后台开启用例评审功能

    一般的,我们写的测试用例都是需要评审的,但是默认的,禅道并没有开启用例评审权限。

    admin账号登录,后台 --- 自定义 --- 用例 ---- 评审流程,勾选开启,然后点击保存。

    接下来 ,由测试人员登录,在创建用例详情中,可以勾选该评审功能。

    需要评审的用例的状态是待评审状态。

    注意,测试人员也能评审自己的用例(也可以使用admin账号取消测试人员的评审测试用例的权限),但一般选择测试主管来评审。

    执行用例并且提交bug

    首先明确,用例执行是一个实际的操作过程,只是在禅道中记录这一过程,并且进行bug管理。

    测试人员登录:

    测试视图 -- 用例列表 ---- 选择执行用例

    提交bug

    测试人员登录,点击执行用例,当用例执行失败,在禅道中需要记录并且提交bug:

    编辑bug信息:

    开发人员访问测试 --- bug,点击指派给我的,bug详情中,点击确认按钮。

    当开发修复bug后,我们测试人员需要进行回归测试。

    如果回归测试成功,选择关闭bug

    如果回归测试失败,重新激活该bug,开发继续修复,我们在进行回归测试,直到回归测试成功,然后关闭该bug。

    参考:https://www.cnblogs.com/Neeo/articles/12620853.html

    postman

    下载和安装

    官网下载,然后点击安装,会自动完成安装,无需别的配置,当安装成功后,会提示你登录/注册账号,建议用账号登录

    postman面板介绍

    常见的请求:

  • 无参get请求
  • 有参数的get请求
  • k:v格式的post请求
  • k:json格式的post请求
  • json格式的post请求
  • 更多内置的变量参考:https://learning.postman.com/docs/postman/variables-and-environments/variables-list/

    参考:https://www.cnblogs.com/Neeo/articles/12186485.html

    python中的断言,assert,断定结果是怎样的,否则就断言失败。

    import requests
    response = requests.get("https://www.baidu.com", timeout=5)
        assert response.status_code == 201
        print('断言成功')
    except AssertionError as e:
        print('断言失败')
    

    在postman中,如何使用断言。

    postman中,可用的模板?

    response body:contains string 检查response body包含字符串 pm.test("Body matches string", function () { pm.expect(pm.response.text()).to.include("string_you_want_to_search"); }); response body:convert XML body to a JSON object response body:将XML转换为JSON对象 var jsonObject = xml2Json(responseBody); response body:is equal to a string 检查响应体等于一个字符串 pm.test("Body is correct", function () { pm.response.to.have.body("response_body_string"); }); response body:JSON value check 检查response body中JSON某字段值 pm.test("Your test name", function () { var jsonData = pm.response.json(); pm.expect(jsonData.value).to.eql(100); }); response headers:content-Type header check 检查content-Type是否包含在header返回 pm.test("Content-Type is present", function () { pm.response.to.have.header("Content-Type"); }); response time is than 200ms 响应时间超过200ms pm.test("Response time is less than 200ms", function () { pm.expect(pm.response.responseTime).to.be.below(200); }); send s request 发送一个请求 pm.sendRequest("https://postman-echo.com/get", function (err, response) { console.log(resp onse.json()); }); set a global variable 设置一个全局变量 pm.globals.set("variable_key", "variable_value"); set an environment variable 设置一个环境变量 pm.environment.set("variable_key", "variable_value"); status code:Code is 200 状态码:代码是200 pm.test("Status code is 200", function () { pm.response.to.have.status(200); }); status code:code name has string 状态码:代码中有指定字符串 pm.test("Status code name has string", function () { pm.response.to.have.status("Created"); }); status code:successful POST request 状态码:成功的post请求 pm.test("Successful POST request", function () { pm.expect(pm.response.code).to.be.oneOf([201,202]); }); use tiny validator for JSON data 为json数据使用tiny验证器 var schema = { "items": { "type": "boolean" } }; var data1 = [true, false]; var data2 = [true, 123]; pm.test('Schema is valid', function() { pm.expect(tv4.validate(data1, schema)).to.be.true; pm.expect(tv4.validate(data2, schema)).to.be.true; });

    常用的断言

    // 状态码断言
    pm.test("判断响应状态码是否是 200", function () {
        pm.response.to.have.status(200);
    // 判断json中的key value是否符合预期,如果你的key是嵌套结构,就点下去
    pm.test("Your test name", function () {
        var jsonData = pm.response.json();
        pm.expect(jsonData.args.k2).to.eql("v2");
    // 判断文本类型的响应文本中,是否包含指定的字段
    pm.test("判断响应体是否包含指定的字段", function () {
        pm.expect(pm.response.text()).to.include("百度一下");
    

    集合公共断言

    创建集合公共断言。

    可以将集合中的一些公共的断言写到集合配置中,在集合中的接口在执行时,会自动的应用上集合配置中的断言。

  • 知道签名接口的加密规则
  • sign:md5(手机号 + 盐 + 时间戳)
  • 知道,在请求之前处理相关的数据
  • // 获取手机号
    var phone = "1212113";
    // 获取盐
    var optCode = "testfan";
    // 获取时间戳
    var timestamp = new Date().getTime();
    // console.log(timestamp);
    // md5 加密
    var md5 = CryptoJS.MD5(phone + optCode + timestamp).toString();
    // console.log(md5);
    // 将加密后的字符串set回环境中
    pm.globals.set("phone", phone);
    pm.globals.set("timestamp", timestamp);
    pm.globals.set("md5", md5);
    

    cookies

    处理cookie有两种方式:

  • 第一种,手动的获取cookies值,然后在需要cookies的请求中,手动携带,注意:你要保证cookie值是有效的。
  • 第二种,postman在发送请求的时候,如果响应中有cookies返回,就会自动的帮我们保存在cookie管理器中。当有向该域名发送请求的时候,postman会自动的帮我们携带cookie值。
  • token

    postman不会帮我们处理token,只能我们自己来完成:

    第一种,用例浏览器登录账号,获取token值,在有需要的接口中,手动添加。

  • 在登录接口访问成功后,从请求体中获取token字段,并且set到全局变量中。
  • // 从响应体中获取token字段: data
    var token = pm.response.json().data;
    // 将token值set到全局的环境中去。
    pm.globals.set("my_token", token);
    
  • 在需要使用token的接口中,在该接口请求发送之前, 从环境中获取token值,填写在请求头中。
  • webservice接口

    参考:http://www.webxml.com.cn/zh_cn/index.aspx

    简单来说,webservice是通过xml进行交互的web请求,本质上也是HTTP请求。

    Web Service也叫XML Web Service WebService是一种可以接收从Internet或者Intranet上的其它系统中传递过来的请求,轻量级的独立的通讯技术。是通过SOAP在Web上提供的软件服务,使用WSDL文件进行说明,并通过UDDI进行注册。

    如何调试webservice接口

  • 在post请求的raw中输入请求的参数,并且,格式选择xml:
  • 由于上一步中,选择xml格式的时候,postman会自动的在请求头中,添加上Content-Type:application/xml,但是webservice接口的content-type不是application/xml而是text/xml.
  • 修改请求content-type值。
  • 就正常的发请求就完了。
  • 集合自动化

    如上图,在启动集合的时候,有如下参数配置:

  • Environment:选择接口执行时依赖的环境。
  • Iterations:表示该集合内的接口运行的次数。
  • Delay:接口执行前延时多少毫秒。
  • Log Response:默认记录所有的响应日志。
  • Data:参数数据,暂时先不管它。
  • Keep variable values:保存在运行过程中产生的变量或者值,选择默认即可。
  • Run collection without using stored cookies:运行中不使用cookie,这里也保持默认。
  • Save cookies after collection run:在集合执行之后,保存cookies,选择默认。
  • Run 集合名称:运行集合。
  • 数据驱动这里:一般轮训次数会自动的根据你的数据行数来定。如果轮训次数大于数据行数,那么在剩余的轮训中,提取的数据就是数据文件的最后一行。

    命令行测试

    安装nodejs:https://www.cnblogs.com/Neeo/articles/11637320.html,注意,nodejs版本不低于10。

    由于自带的npm命令受网络影响严重,我们需要配置一个国内的淘宝镜像:cnpm,终端(管理员权限)执行如下命令:

    npm install -g cnpm --registry=https://registry.npm.taobao.org
    
  • 测试vnpm是否安装成功:
  • cnpm -v
    
  • 使用cnpm来安装Newman插件:
  • cnpm install newman --global
    newman -v
    

    本地部署Java项目

  • 要安装java jdk(Windows/Mac):java jdk安装:https://www.cnblogs.com/Neeo/articles/11954283.html
  • 下载apache-tomcat-8.5.38.zip压缩包,解压到你的本机上,一个没有中文,空格的目录中。
  • 配置相关的环境变量。
  • 其他平台配置方式:https://www.cnblogs.com/Neeo/articles/12168651.html

    打开tomcat的启动文件,一闪即逝,意思是你的Java环境配置的有问题,解决办法:

  • 检擦你的环境变量是否配置好了。
  • 单元测试自动化

    所谓的单元测试(Unit Test)是根据特定的输入数据,针对程序代码中的最小实体单元的输入输出的正确性进行验证测试的过程。所谓的最小实体单元就是组织项目代码的最基本代码结构:函数,类,模块等。在Python中比较知名的单元测试模块:

  • unittest
  • pytest
  • doctest
  • php phpunit
    java javaunit
    

    参考内容:

  • 单元测试基础 | unittest框架 | mock数据 | Moco框架 | 生成测试报告 | 发送测试报告邮件 | pytest框架 | ddt
  • 所谓的测试用例(Test Case),就是执行测试的依据和记录,把测试应用程序的操作步骤用文档的形式描述出来的一份文档。文档的格式可以是Excel、markdown、html、xmind网页。

    一份合格的测试用例有利于测试人员理清测试思路,确保需要测试的功能周全没有遗漏,方便测试工作的开展和评估测试工作量,同时还可以便于测试人员记录测试数据和测试工作进度,为后续的回归测试提供样本参考,提升测试效率以及后续测试工作的交接。

    那么一份合格的测试用例长什么样子或有什么内容呢?

    一份合格的测试用例,应该包含测试时间、测试人员、测试模块名功能点名称用例ID用例说明(测试目的)前置条件输入数据预期结果测试结果(输出结果、实际结果)等。注意:加粗内容为必备的测试用例八要素。

    参考文档:

    在实际工作中,因为缺陷报告与测试用例作用相似,因此有时候会合并一起或只选择其中一种。

    那么在工作中,我们一般都应该编写测试用例或者应该怎么设计测试用例来完成我们的测试工作呢?实际上在工作中,测试人员都是基于测试用例的7种基本设计方法来设计与编写测试用例的:

  • 等价类划分法:根据输入数据的有效性与无效性设计测试用例。
  • 边界值分析法:对等价类划分法的一个补充,从等价类的边缘值(临界点)去寻找错误,基于这些错误来设计测试用例。
  • 判定表法:把输入数据的各种可能情况进行组合罗列成一个判断表,以判断表来设计测试用例。
  • 因果图法:用图解的方式表示输入数据的各种组合关系,以此写出判定表,从而设计相应的测试用例。
  • 正交表法:基于正交表来设计测试用例。
  • 场景法:基于流程图展示业务流程或功能的调用流程,对流程图的走向路径设计测试用例。
  • 错误推测法:基于经验和直觉,找出程序中认为可能出现的错误来设计测试用例。
  • 一般在工作中,我们比较常用的是等价类划分法与判定表法。

    等价类划分法

    等价类划分法就是按照测试要求,把具有共同特征的测试数据划分为2类:有效等价类和无效等价类,把测试数据进行分类以后设计测试用例。

  • 有效等价类,就是符合程序使用要求或调用代码要求的,能正确使用程序或调用代码的一类数据。
  • 无效等价类,就是不符合程序使用要求或调用代码要求的,会导致程序出现异常或结果不正确的一类数据。
  • 使用等价类划分法,可以让我们设计的测试工作更加科学有依据,避免出现穷举测试的情况,减少测试用例的数量。

    例如,注册功能中用户名的测试用例,如果功能需求中,要求用户名必须长度为3-11个长度的字符。

    判定表是分析和表达多逻辑条件下执行不同操作的情况的工具。而软件测试中的判定表法,就是把输入数据的各种可能情况进行组合罗列成一个判断表格,以判断表来设计测试用例。

    判定表的表结构一般有如下2种:横向判断表与纵向判定表。

    横向判断表:

    单元测试框架-Unittest

    Unittest是Python开发中常用于单元测试的内置框架,免安装使用简单方便,其设计的灵感来源于Java的单元测试框架-Junit。

    Unittest具备完整的测试结构,支持自动化测试的执行,对测试用例进行组织,并且提供了丰富的断言方法,还提供生成测试报告。

    官方文档:https://docs.python.org/zh-cn/3/library/unittest.html

    import unittest
    print(dir(unittest))
    

    上面的代码中,我们就引入了Unittest模块, 同时可以通过打印发现Unittest框架中内置了大量的工具成员。这些工具成员中除了以下5个以外,其他的都不怎么常用。

    TestCase(测试用例)

    是unittest中最重要的一个类,用于编写测试用例类,是所有测试用例类的父类,实现了测试用例的基本代码。

    TestSuite(测试套件、测试集)

    可以把多个TestCase组织、打包集成到一个测试集中一起执行,TestSuite可以实现多个测试用例的执行。

    TextTestRunner(测试运行器)

    TestSuite本身不具备执行的功能,所以使用TextTestRunner执行测试套件和输出测试结果。

    TestLoader(测试加载器)

    用于加载测试用例TestCase,并生成测试套件TestSuite,实现自动从代码中加载大量测试用例到测试套件中。

    TestFixture(测试脚手架)

    所谓的测试脚手架就是为了开展一项或多项测试所需要进行的准备工作,以及所有相关的清理操作。测试脚手架实际上会在执行一些测试代码之前与之后,让我们编写一些初始化和销毁的代码。

    测试用例-TestCase

    前面讲到TestCase就是提供给我们编写测试用例的测试代码的,那么怎么编写一个测试用例?需要4个步骤即可。

    导入unittest模块

    import unittest
    

    定义测试用例类

    import unittest
    class 测试用例类名(unittest.TestCase): # 所有的测试用例类都必须直接或者间接继承unittest.TestCase.
        """测试用例"""
    

    定义测试用例方法(此处的测试用例方法,就是上面所说的测试用例设计方法中的一行信息的测试代码)

    import unittest
    class 测试用例类名(unittest.TestCase):
        """测试用例"""
        # ....
        def test_测试方法名(参数):  # 测试方法必须以test开头或test_开头
        # ....
    

    在实际工作中,我们肯定是在项目中进行测试代码的编写或单独编写一个测试项目,但是我们现在刚开始学习,所以我们可以先编写一个例子代码,对其进行测试,以达到学习的目的。

    unittest_01_测试用例的编写.py,代码:

    import unittest
    # 被测试的代码单元
    def add(x,y):
        return x+y
    class FuncTest(unittest.TestCase):
        """测试用例"""
        def test_01(self):
            print(add(10, 20))
        def test_02(self):
            print(add("hello", "world"))
        # def test_03(self):
        #     print(add("hello", 20))
    # 因为pycharm本身内置了执行unittest的功能,所以不适用以下代码也能执行,但是终端下或者使用其他的代码编辑器时,则需要加上。
    if __name__ == '__main__':
        unittest.main()
    

    测试套件-TestSuite

    前面我们将到测试套件,主要用于把多个测试用例类打包集成到一个测试集中一起执行。工作中,一个项目往往需要编写非常多的测试用例,而那么多的测试用例也不可能只编写在一个文件中,此时就需要使用测试套件了。2个步骤:

    通过unittest.TestSuite实例化测试套件对象

    suite = unittest.TestSuite()
    - addTest,一个一个添加
    - addTests,批量添加
    - suite.countTestCases(),返回suite中用例个数
    # # 1. 添加测试用例方法
    # suite.addTest(unittest_01.FuncTest("test_01"))
    # suite.addTest(unittest_01.FuncTest("test_02"))
    # # 2. 批量添加测试用例方法
    # test_data = (unittest_01.FuncTest("test_01"), unittest_01.FuncTest("test_02"))
    # suite.addTests(test_data)
    # # 3. 添加测试用例类
    # suite.addTest(unittest.makeSuite(unittest_01.FuncTest))
    # 4. 批量添加测试用例类
    test_data = (unittest.makeSuite(unittest_01.FuncTest),  unittest.makeSuite(unittest_01.FuncTest))
    suite.addTests(test_data)
    

    TestSuite的作用仅仅是把多个测试用例打包集成到一块,但是并没有提供批量执行测试用例的方法,所以我们需要使用TextTestRunner了。

    测试运行器-TextTestRunner

    前面说过,TextTestRunner是用于执行测试用例、测试套件和输出测试结果的。2个步骤:

    实例化运行器对象

    通过run方法执行测试

    unittest_03_测试运行器基本使用.py,代码:

    import unittest
    import unittest_01_测试用例的编写 as unittest_01
    suite = unittest.TestSuite()
    # # 添加测试用例方法
    # suite.addTest(unittest_01.FuncTest("test_01"))
    # suite.addTest(unittest_01.FuncTest("test_02"))
    # # 批量添加测试用例方法
    # test_data = (unittest_01.FuncTest("test_01"), unittest_01.FuncTest("test_02"))
    # suite.addTests(test_data)
    # # 添加测试用例类
    # suite.addTest(unittest.makeSuite(unittest_01.FuncTest))
    # 批量添加测试用例类
    test_data = (unittest.makeSuite(unittest_01.FuncTest),  unittest.makeSuite(unittest_01.FuncTest))
    suite.addTests(test_data)
    if __name__ == '__main__':
        runner = unittest.TextTestRunner()
        runner.run(suite)
    

    测试加载器-TestLoader

    前面说过,用于加载测试用例TestCase,并生成测试套件TestSuite,实现自动从代码中加载大量测试用例到测试套件中。2个步骤:

    实例化unittest.TestLoader对象

    loader = unittest.TestLoader()
    

    使用discover方法自动搜索指定目录下指定文件格式的python模块,并把查找到的测试用例组装打包集成到测试组件作为返回值。

    loader.discover(目录路径, pattern="文件名格式")
    

    注意:pattern支持*号表示0到多个字符。

    发现其他目录中的脚本用例:

  • unittest.TestLoader().loadTestsFromModule,找到指定模块下面的TestCase的子类,获取其中以test开头的用例。
  • unittest.TestLoader().loadTestsFromModule,获取指定模块中的,指定的用例
  • import unittest
    suite = unittest.TestLoader().loadTestsFromModule(ff_case)
        suite = unittest.TestLoader().loadTestsFromName(
            name="ff_case.TestCase.test_case_01",
            module=ff_case.TestCase
        suite = unittest.TestLoader().loadTestsFromNames(
            names=[
                "ff_case.TestCase.test_case_01",
                "ff_case.TestCase.test_case_02",
            module=ff_case.TestCase
    

    suite = unittest.TestLoader().discover(
    top_level_dir=index_dir, # 顶级目录
    start_dir=SCRIPTS_DIR, # 起始目录
    pattern='unittest_*' # 以unittest开头的python文件

    top_level_dir和start_dir的关系:

  • top_level_dir == start_dir,没问题
  • top_level_dir > start_dir, 没问题
  • top_level_dir < start_dir, 有问题
  • 注意,discover只会收集Python的包(__init__.py)中以pattern开头的脚本,再找脚本中unittest.TestCase的子类中的以test开头的测试用例

    unittest_04_测试加载器基本使用.py,代码:

    import unittest
    loader = unittest.TestLoader()
    # 在当前目录下,搜索以unittest开头作为文件名的所有python文件,并把文件中的测试用例类打包集成到测试套件中
    suite =loader.discover("./", pattern="unittest*.py")
    if __name__ == '__main__':
        runner = unittest.TextTestRunner()
        runner.run(suite)
    

    测试脚手架-TestFixture

    前面提到,测试脚手架会在执行一些测试代码之前与之后,让我们编写一些初始化和销毁的代码,主要分三个级别:

    方法级别:在方法执行前与执行后都提供自动调用的实例方法

    setUp和tearDown

    类级别:在类执行前与执行后都提供自动调用的类方法,不管类中有多少方法,只执行一次。

    setUpClass和tearDownClass

    模块级别:在模块执行前与执行后都提供自动调用的函数,不管模块中有多少类或方法,只执行一次。

    setUpModule和tearDownModule

    方法级别的脚手架setUp与tearDown

    在测试用例类中提供了2个固定名字的实例方法(setUp与tearDown),用于完成方法执行前与执行后的操作。

    unittest_05_测试脚手架_方法级别的脚手架.py,代码:

    import unittest
    # 被测试的代码单元
    def add(x,y):
        return x+y
    class AddTest(unittest.TestCase):
        """测试用例"""
        def setUp(self):
            print("每个方法执行前都会执行一遍setUp实例方法,用于完成通用的前置操作或初始化工作")
        def tearDown(self):
            print("每个方法执行后都会执行一遍tearDown实例方法,用于完成通用的后置操作或销毁工作")
        def test_01(self):
            print(add(10, 20))
        def test_03(self):
            print(add("hello", 20))
    # 因为pycharm本身内置了执行unittest的功能,所以不适用以下代码也能执行,但是终端下或者使用其他的代码编辑器时,则需要加上。
    if __name__ == '__main__':
        unittest.main()
    
    类级别的脚手架setUpClass与tearDownClass

    在测试用例类中提供了2个固定名字的类方法(setUpClass与tearDownClass),用于完成类执行前与执行后的操作。

    unittest_06_测试脚手架_类级别的脚手架.py,代码:

    import unittest
    # 被测试的代码单元
    def add(x,y):
        return x+y
    class AddTest(unittest.TestCase):
        """测试用例"""
        @classmethod
        def setUpClass(cls):
            print("当前类执行前都会执行一遍setUpClass类方法,用于完成通用的前置操作或初始化工作")
        @classmethod
        def tearDownClass(cls):
            print("当前类执行后都会执行一遍tearDownClass类方法,用于完成通用的后置操作或销毁工作")
        def test_01(self):
            print(add(10, 20))
        def test_03(self):
            print(add("hello", 20))
    # 因为pycharm本身内置了执行unittest的功能,所以不适用以下代码也能执行,但是终端下或者使用其他的代码编辑器时,则需要加上。
    if __name__ == '__main__':
        unittest.main()
    
    模块级别的脚手架setUpModule与tearDownModule

    在测试用例类中提供了2个固定名字的函数(setUpModule与tearDownModule),用于完成类执行前与执行后的操作。

    unittest_07_测试脚手架_模块级别的脚手架.py,代码:

    import unittest
    def setUpModule():
        print("当前模块执行前都会执行一遍setUpModule函数,用于完成通用的前置操作或初始化工作")
    def tearDownModule():
        print("当前模块执行前都会执行一遍tearDownModule函数,用于完成通用的前置操作或初始化工作")
    # 被测试的代码单元
    def add(x, y):
        return x + y
    class AddTest1(unittest.TestCase):
        """测试用例"""
        @classmethod
        def setUpClass(cls):
            print("当前类执行前都会执行一遍setUpClass类方法,用于完成通用的前置操作或初始化工作")
        @classmethod
        def tearDownClass(cls):
            print("当前类执行后都会执行一遍tearDownClass类方法,用于完成通用的后置操作或销毁工作")
        def test_01(self):
            print(add(10, 20))
    class AddTest2(unittest.TestCase):
        """测试用例"""
        @classmethod
        def setUpClass(cls):
            print("当前类执行前都会执行一遍setUp方法,用于完成通用的前置操作或初始化工作")
        @classmethod
        def tearDownClass(cls):
            print("当前类执行后都会执行一遍tearDown方法,用于完成通用的后置操作或销毁工作")
        def test_03(self):
            print(add("hello", 20))
    # 因为pycharm本身内置了执行unittest的功能,所以不适用以下代码也能执行,但是终端下或者使用其他的代码编辑器时,则需要加上。
    if __name__ == '__main__':
        unittest.main()
    

    生成HTML格式测试报告(很少使用,基本不使用)

    使用HTMLTestRunner模块可以直接生成HTML格式的报告。HTMLTestRunner是一个不再维护的第三方的模块,通过pip工具安装不了,只能下载后手动导入。

    HTMLTestRunner官网:http://tungwaiyip.info/software/HTMLTestRunner.html

    HTMLTestRunner下载:http://tungwaiyip.info/software/HTMLTestRunner_0_8_2/HTMLTestRunner.py

    因为HTMLTestRunner是python2模块的,所以在python3.x以后需要做些修改才可以使用,修改后版本代码如下:

    A TestRunner for use with the Python unit testing framework. It generates a HTML report to show the result at a glance. The simplest way to use this is to invoke its main method. E.g. import unittest import HTMLTestRunner ... define your tests ... if __name__ == '__main__': HTMLTestRunner.main() For more customization options, instantiates a HTMLTestRunner object. HTMLTestRunner is a counterpart to unittest's TextTestRunner. E.g. # output to a file fp = file('my_report.html', 'wb') runner = HTMLTestRunner.HTMLTestRunner( stream=fp, title='My unit test', description='This demonstrates the report output by HTMLTestRunner.' # Use an external stylesheet. # See the Template_mixin class for more customizable options runner.STYLESHEET_TMPL = '<link rel="stylesheet" href="my_stylesheet.css" type="text/css">' # run the test runner.run(my_test_suite) ------------------------------------------------------------------------ All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name Wai Yip Tung nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # URL: http://tungwaiyip.info/software/HTMLTestRunner.html __author__ = "Wai Yip Tung" __version__ = "0.8.2" Change History Version 0.8.2 * Show output inline instead of popup window (Viorel Lupu). Version in 0.8.1 * Validated XHTML (Wolfgang Borgert). * Added description of test classes and test cases. Version in 0.8.0 * Define Template_mixin class for customization. * Workaround a IE 6 bug that it does not treat <script> block as CDATA. Version in 0.7.1 * Back port to Python 2.3 (Frank Horowitz). * Fix missing scroll bars in detail log (Podi). # TODO: color stderr # TODO: simplify javascript using ,ore than 1 class in the class attribute? import datetime import io import sys import time import unittest from xml.sax import saxutils # ------------------------------------------------------------------------ # The redirectors below are used to capture output during testing. Output # sent to sys.stdout and sys.stderr are automatically captured. However # in some cases sys.stdout is already cached before HTMLTestRunner is # invoked (e.g. calling logging.basicConfig). In order to capture those # output, use the redirectors for the cached stream. # e.g. # >>> logging.basicConfig(stream=HTMLTestRunner.stdout_redirector) class OutputRedirector(object): """ Wrapper to redirect stdout or stderr """ def __init__(self, fp): self.fp = fp def write(self, s): self.fp.write(s) def writelines(self, lines): self.fp.writelines(lines) def flush(self): self.fp.flush() stdout_redirector = OutputRedirector(sys.stdout) stderr_redirector = OutputRedirector(sys.stderr) # ---------------------------------------------------------------------- # Template class Template_mixin(object): Define a HTML template for report customerization and generation. Overall structure of an HTML report +------------------------+ |<html> | | <head> | | | | STYLESHEET | | +----------------+ | | | | | | +----------------+ | | | | </head> | | | | <body> | | | | HEADING | | +----------------+ | | | | | | +----------------+ | | | | REPORT | | +----------------+ | | | | | | +----------------+ | | | | ENDING | | +----------------+ | | | | | | +----------------+ | | | | </body> | |</html> | +------------------------+ STATUS = { 0: 'pass', 1: 'fail', 2: 'error', DEFAULT_TITLE = 'Unit Test Report' DEFAULT_TESTER = 'Unit Test' DEFAULT_DESCRIPTION = '' # ------------------------------------------------------------------------ # HTML Template HTML_TMPL = r"""<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <title>%(title)s</title> <meta name="generator" content="%(generator)s"/> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> %(stylesheet)s </head> <script language="javascript" type="text/javascript"><!-- output_list = Array(); /* level - 0:Summary; 1:Failed; 2:All */ function showCase(level) { trs = document.getElementsByTagName("tr"); for (var i = 0; i < trs.length; i++) { tr = trs[i]; id = tr.id; if (id.substr(0,2) == 'ft') { if (level < 1) { tr.className = 'hiddenRow'; else { tr.className = ''; if (id.substr(0,2) == 'pt') { if (level > 1) { tr.className = ''; else { tr.className = 'hiddenRow'; function showClassDetail(cid, count) { var id_list = Array(count); var toHide = 1; for (var i = 0; i < count; i++) { tid0 = 't' + cid.substr(1) + '.' + (i+1); tid = 'f' + tid0; tr = document.getElementById(tid); if (!tr) { tid = 'p' + tid0; tr = document.getElementById(tid); id_list[i] = tid; if (tr.className) { toHide = 0; for (var i = 0; i < count; i++) { tid = id_list[i]; if (toHide) { document.getElementById('div_'+tid).style.display = 'none' document.getElementById(tid).className = 'hiddenRow'; else { document.getElementById(tid).className = ''; function showTestDetail(div_id){ var details_div = document.getElementById(div_id) var displayState = details_div.style.display // alert(displayState) if (displayState != 'block' ) { displayState = 'block' details_div.style.display = 'block' else { details_div.style.display = 'none' function html_escape(s) { s = s.replace(/&/g,'&amp;'); s = s.replace(/</g,'&lt;'); s = s.replace(/>/g,'&gt;'); return s; /* obsoleted by detail in <div> function showOutput(id, name) { var w = window.open("", //url name, "resizable,scrollbars,status,width=800,height=450"); d = w.document; d.write("<pre>"); d.write(html_escape(output_list[id])); d.write("\n"); d.write("<a href='javascript:window.close()'>close</a>\n"); d.write("</pre>\n"); d.close(); --></script> %(heading)s %(report)s %(ending)s </body> </html> # variables: (title, generator, stylesheet, heading, report, ending) # ------------------------------------------------------------------------ # Stylesheet # alternatively use a <link> for external style sheet, e.g. # <link rel="stylesheet" href="$url" type="text/css"> STYLESHEET_TMPL = """ <style type="text/css" media="screen"> body { font-family: verdana, arial, helvetica, sans-serif; font-size: 80%; } table { font-size: 100%; } pre { } /* -- heading ---------------------------------------------------------------------- */ font-size: 16pt; color: gray; .heading { margin-top: 0ex; margin-bottom: 1ex; .heading .attribute { margin-top: 1ex; margin-bottom: 0; .heading .description { margin-top: 4ex; margin-bottom: 6ex; /* -- css div popup ------------------------------------------------------------------------ */ a.popup_link { a.popup_link:hover { color: red; .popup_window { display: none; position: relative; left: 0px; top: 0px; /*border: solid #627173 1px; */ padding: 10px; background-color: #E6E6D6; font-family: "Lucida Console", "Courier New", Courier, monospace; text-align: left; font-size: 8pt; width: 500px; /* -- report ------------------------------------------------------------------------ */ #show_detail_line { margin-top: 3ex; margin-bottom: 1ex; #result_table { width: 80%; border-collapse: collapse; border: 1px solid #777; #header_row { font-weight: bold; color: white; background-color: #777; #result_table td { border: 1px solid #777; padding: 2px; #total_row { font-weight: bold; } .passClass { background-color: #6c6; } .failClass { background-color: #c60; } .errorClass { background-color: #c00; } .passCase { color: #6c6; } .failCase { color: #c60; font-weight: bold; } .errorCase { color: #c00; font-weight: bold; } .hiddenRow { display: none; } .testcase { margin-left: 2em; } /* -- ending ---------------------------------------------------------------------- */ #ending { </style> # ------------------------------------------------------------------------ # Heading HEADING_TMPL = """<div class='heading'> <h1>%(title)s</h1> %(parameters)s <p class='description'>%(description)s</p> """ # variables: (title, parameters, description) HEADING_ATTRIBUTE_TMPL = """<p class='attribute'><strong>%(name)s:</strong> %(value)s</p> """ # variables: (name, value) # ------------------------------------------------------------------------ # Report REPORT_TMPL = """ <p id='show_detail_line'>Show <a href='javascript:showCase(0)'>Summary</a> <a href='javascript:showCase(1)'>Failed</a> <a href='javascript:showCase(2)'>All</a> <table id='result_table'> <colgroup> <col align='left' /> <col align='right' /> <col align='right' /> <col align='right' /> <col align='right' /> <col align='right' /> </colgroup> <tr id='header_row'> <td>Test Group/Test case</td> <td>Count</td> <td>Pass</td> <td>Fail</td> <td>Error</td> <td>View</td> %(test_list)s <tr id='total_row'> <td>Total</td> <td>%(count)s</td> <td>%(Pass)s</td> <td>%(fail)s</td> <td>%(error)s</td> <td>&nbsp;</td> </table> """ # variables: (test_list, count, Pass, fail, error) REPORT_CLASS_TMPL = r""" <tr class='%(style)s'> <td>%(desc)s</td> <td>%(count)s</td> <td>%(Pass)s</td> <td>%(fail)s</td> <td>%(error)s</td> <td><a href="javascript:showClassDetail('%(cid)s',%(count)s)">Detail</a></td> """ # variables: (style, desc, count, Pass, fail, error, cid) REPORT_TEST_WITH_OUTPUT_TMPL = r""" <tr id='%(tid)s' class='%(Class)s'> <td class='%(style)s'><div class='testcase'>%(desc)s</div></td> <td colspan='5' align='center'> <!--css div popup start--> <a class="popup_link" onfocus='this.blur();' href="javascript:showTestDetail('div_%(tid)s')" > %(status)s</a> <div id='div_%(tid)s' class="popup_window"> <div style='text-align: right; color:red;cursor:pointer'> <a onfocus='this.blur();' onclick="document.getElementById('div_%(tid)s').style.display = 'none' " > [x]</a> %(script)s <!--css div popup end--> """ # variables: (tid, Class, style, desc, status) REPORT_TEST_NO_OUTPUT_TMPL = r""" <tr id='%(tid)s' class='%(Class)s'> <td class='%(style)s'><div class='testcase'>%(desc)s</div></td> <td colspan='5' align='center'>%(status)s</td> """ # variables: (tid, Class, style, desc, status) REPORT_TEST_OUTPUT_TMPL = r""" %(id)s: %(output)s """ # variables: (id, output) # ------------------------------------------------------------------------ # ENDING ENDING_TMPL = """<div id='ending'>&nbsp;</div>""" # -------------------- The end of the Template class ------------------- TestResult = unittest.TestResult class _TestResult(TestResult): # note: _TestResult is a pure representation of results. # It lacks the output and reporting ability compares to unittest._TextTestResult. def __init__(self, verbosity=1): TestResult.__init__(self) self.stdout0 = None self.stderr0 = None self.success_count = 0 self.failure_count = 0 self.error_count = 0 self.verbosity = verbosity # result is a list of result in 4 tuple # result code (0: success; 1: fail; 2: error), # TestCase object, # Test output (byte string), # stack trace, self.result = [] def startTest(self, test): TestResult.startTest(self, test) # just one buffer for both stdout and stderr self.outputBuffer = io.StringIO() stdout_redirector.fp = self.outputBuffer stderr_redirector.fp = self.outputBuffer self.stdout0 = sys.stdout self.stderr0 = sys.stderr sys.stdout = stdout_redirector sys.stderr = stderr_redirector def complete_output(self): Disconnect output redirection and return buffer. Safe to call multiple times. if self.stdout0: sys.stdout = self.stdout0 sys.stderr = self.stderr0 self.stdout0 = None self.stderr0 = None return self.outputBuffer.getvalue() def stopTest(self, test): # Usually one of addSuccess, addError or addFailure would have been called. # But there are some path in unittest that would bypass this. # We must disconnect stdout in stopTest(), which is guaranteed to be called. self.complete_output() def addSuccess(self, test): self.success_count += 1 TestResult.addSuccess(self, test) output = self.complete_output() self.result.append((0, test, output, '')) if self.verbosity > 1: sys.stderr.write('ok ') sys.stderr.write(str(test)) sys.stderr.write('\n') else: sys.stderr.write('.') def addError(self, test, err): self.error_count += 1 TestResult.addError(self, test, err) _, _exc_str = self.errors[-1] output = self.complete_output() self.result.append((2, test, output, _exc_str)) if self.verbosity > 1: sys.stderr.write('E ') sys.stderr.write(str(test)) sys.stderr.write('\n') else: sys.stderr.write('E') def addFailure(self, test, err): self.failure_count += 1 TestResult.addFailure(self, test, err) _, _exc_str = self.failures[-1] output = self.complete_output() self.result.append((1, test, output, _exc_str)) if self.verbosity > 1: sys.stderr.write('F ') sys.stderr.write(str(test)) sys.stderr.write('\n') else: sys.stderr.write('F') class HTMLTestRunner(Template_mixin): def __init__(self, stream=sys.stdout, verbosity=1, title=None, description=None, tester=None): self.stream = stream self.verbosity = verbosity if title is None: self.title = self.DEFAULT_TITLE else: self.title = title if description is None: self.description = self.DEFAULT_DESCRIPTION else: self.description = description self.startTime = datetime.datetime.now() if tester is None: self.tester = self.DEFAULT_TITLE else: self.tester = tester def run(self, test): "Run the given test case or test suite." result = _TestResult(self.verbosity) test(result) self.stopTime = datetime.datetime.now() self.generateReport(test, result) print (sys.stderr, "\nTime Elapsed: %s" %(self.stopTime-self.startTime)) return result def sortResult(self, result_list): # unittest does not seems to run in any particular order. # Here at least we want to group them together by class. rmap = {} classes = [] for n,t,o,e in result_list: cls = t.__class__ if not cls in rmap: rmap[cls] = [] classes.append(cls) rmap[cls].append((n,t,o,e)) r = [(cls, rmap[cls]) for cls in classes] return r def getReportAttributes(self, result): Return report attributes as a list of (name, value). Override this to add custom attributes. startTime = str(self.startTime)[:19] duration = str(self.stopTime - self.startTime) status = [] if result.success_count: status.append('Pass %s' % result.success_count) if result.failure_count: status.append('Failure %s' % result.failure_count) if result.error_count: status.append('Error %s' % result.error_count ) if status: status = ' '.join(status) else: status = 'none' return [ ('Start Time', startTime), ('Duration', duration), ('Status', status), ('Tester', self.tester), def generateReport(self, test, result): report_attrs = self.getReportAttributes(result) generator = 'HTMLTestRunner %s' % __version__ stylesheet = self._generate_stylesheet() heading = self._generate_heading(report_attrs) report = self._generate_report(result) ending = self._generate_ending() output = self.HTML_TMPL % dict( title = saxutils.escape(self.title), generator = generator, stylesheet = stylesheet, heading = heading, report = report, ending = ending, self.stream.write(output.encode('utf8')) def _generate_stylesheet(self): return self.STYLESHEET_TMPL def _generate_heading(self, report_attrs): a_lines = [] for name, value in report_attrs: line = self.HEADING_ATTRIBUTE_TMPL % dict( name = saxutils.escape(name), value = saxutils.escape(value), a_lines.append(line) heading = self.HEADING_TMPL % dict( title = saxutils.escape(self.title), parameters = ''.join(a_lines), description = saxutils.escape(self.description), return heading def _generate_report(self, result): rows = [] sortedResult = self.sortResult(result.result) for cid, (cls, cls_results) in enumerate(sortedResult): # subtotal for a class np = nf = ne = 0 for n,t,o,e in cls_results: if n == 0: np += 1 elif n == 1: nf += 1 else: ne += 1 # format class description if cls.__module__ == "__main__": name = cls.__name__ else: name = "%s.%s" % (cls.__module__, cls.__name__) doc = cls.__doc__ and cls.__doc__.split("\n")[0] or "" desc = doc and '%s: %s' % (name, doc) or name row = self.REPORT_CLASS_TMPL % dict( style = ne > 0 and 'errorClass' or nf > 0 and 'failClass' or 'passClass', desc = desc, count = np+nf+ne, Pass = np, fail = nf, error = ne, cid = 'c%s' % (cid+1), rows.append(row) for tid, (n,t,o,e) in enumerate(cls_results): self._generate_report_test(rows, cid, tid, n, t, o, e) report = self.REPORT_TMPL % dict( test_list = ''.join(rows), count = str(result.success_count+result.failure_count+result.error_count), Pass = str(result.success_count), fail = str(result.failure_count), error = str(result.error_count), return report def _generate_report_test(self, rows, cid, tid, n, t, o, e): # e.g. 'pt1.1', 'ft1.1', etc has_output = bool(o or e) tid = (n == 0 and 'p' or 'f') + 't%s.%s' % (cid+1,tid+1) name = t.id().split('.')[-1] doc = t.shortDescription() or "" desc = doc and ('%s: %s' % (name, doc)) or name tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL or self.REPORT_TEST_NO_OUTPUT_TMPL # o and e should be byte string because they are collected from stdout and stderr? if isinstance(o,str): # TODO: some problem with 'string_escape': it escape \n and mess up formating # uo = unicode(o.encode('string_escape')) else: uo = o ue = e script = self.REPORT_TEST_OUTPUT_TMPL % dict( id = tid, output = saxutils.escape(uo+ue), row = tmpl % dict( tid = tid, Class = (n == 0 and 'hiddenRow' or 'none'), style = n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'none'), desc = desc, script = script, status = self.STATUS[n], rows.append(row) if not has_output: return def _generate_ending(self): return self.ENDING_TMPL ############################################################################## # Facilities for running tests from the command line ############################################################################## # Note: Reuse unittest.TestProgram to launch test. In the future we may # build our own launcher to support more specific command line # parameters like test title, CSS, etc. class TestProgram(unittest.TestProgram): A variation of the unittest.TestProgram. Please refer to the base class for command line parameters. def runTests(self): # Pick HTMLTestRunner as the default test runner. # base class's testRunner parameter is not useful because it means # we have to instantiate HTMLTestRunner before we know self.verbosity. if self.testRunner is None: self.testRunner = HTMLTestRunner(verbosity=self.verbosity) unittest.TestProgram.runTests(self) main = TestProgram ############################################################################## # Executing this module from the command line ############################################################################## if __name__ == "__main__": main(module=None)

    打印HTML格式的测试报告,unittest_08_打印HTML格式的测试报告.py,代码:

    import unittest
    from HTMLTestRunner import HTMLTestRunner
    import unittest_01_测试用例的编写 as unittest_01
    suite = unittest.TestSuite()
    test_data = (unittest.makeSuite(unittest_01.FuncTest),  unittest.makeSuite(unittest_01.FuncTest))
    suite.addTests(test_data)
    if __name__ == '__main__':
        # 生成html报告
        with open("test_report.html", "wb") as file:
            runner = HTMLTestRunner(
                stream=file,
                title="单元测试的HTML格式报告",
                description="python单元测试报告",
                tester="墨落"
            runner.run(suite)
    

    断言(assertion)是一种在程序中的判断测试用例执行结果是否符合预期结果的方式,所以断言也被称之为“期望”。当程序执行到断言的位置时,对应的断言应该为真。若断言不为真时,程序会中止执行,并给出错误信息。

    unittest中常用的断言方法(加粗为重要方法):

    # 断言结果是否与预期内容相同 # self.assertEqual(res, 3, msg="断言失败!一般会错误的结果与原因") # self.assertEqual(res, 2, msg="断言失败!一般会错误的结果与原因") self.assertIn(res, [1, 2], msg="断言失败!一般会错误的结果与原因") if __name__ == '__main__': unittest.main()

    用例执行成功用.表示,执行失败是F表示。
    unittest.main()会自动的找到当前模块的unittest.TestCase的子类,然后找该子类内部以test开头的用例名,完事去一一的执行它们。

    针对开发中有时候针对不同环境或者不同的时间段,不同的代码版本,有时候部分测试用例不希望被执行,则可以使用跳过。

    @ unittest.skip(reason)   # 跳过用例的描述
    @ unittest.skipif(condition, reason)   # 跳过的条件,跳过的原因
    

    unittest_10_跳过.py,代码:

    import unittest
    class MyCase(unittest.TestCase):
        def test_case_01(self):
            self.assertTrue(1)
        @unittest.skip(reason='无条件跳过')
        def test_case_02(self):
            self.assertTrue("")
        @unittest.skipIf(condition=3 < 2, reason='有条件跳过')
        def test_case_03(self):
            self.assertTrue(0)
    if __name__ == '__main__':
        unittest.main(verbosity=2)
    

    在输出的窗口中,跳过的用例用s表示;断言成功.表示;断言失败F表示。

    当需要使用多组不同的测试数据测试同一个方法时,可以使用unittest参数化来解决。常用的参数化方法有ddt、parameterized

    pip install parameterized
    

    unittest_11_参数化.py,代码:

    import unittest
    from parameterized import parameterized
    def add(x, y):
        return x + y
    version = (2, 7, 0)
    class AddTest(unittest.TestCase):
        def setUp(self):
            print("setUP执行....")
        @parameterized.expand([(10,20), ("a","B"), (50, 20)])
        def test_00(self, x, y):
            res = add(x, y)
            self.assertIn(res, [1, 30, "aB", 70], msg="断言失败!一般会错误的结果与原因")
        # def test_01(self):
        #     res = add(1, 2)
        #     self.assertIn(res, [1, 3], msg="断言失败!一般会错误的结果与原因")
        # def test_02(self):
        #     res = add("a", "B")
        #     self.assertEqual(res, "aB", msg="断言失败!一般会错误的结果与原因")
        # def test_03(self):
        #     print(add("a", 20))
    if __name__ == '__main__':
        unittest.main()
    

    数据驱动测试

    Data-Driven Tests(DDT)即数据驱动测试,可以实现多个数据对同一个方法进行测试,达到数据和测试代码分离,目的是为了减少测试用例的数量。

    pip install ddt
    

    直接传递单个数据

    unittest_12_参数化_基于ddt直接传递数据.py,代码:

    import unittest
    from ddt import ddt, data
    def add(a,b):
        return a+b
    class AddTest(unittest.TestCase):
        # # 单次传递一个数据到测试用例方法中
        # @data(100)
        # @data([1,2,3,4])
        # @data({"a":1,"b":2})
        # @data((1,2,3))
        # # 多次传递一个数据到测试用例方法中
        # @data(*["a","b","c"]) # 字符串
        # @data(*[{"a":1}, {"a":2}, {"a":3}]) # 字典
        # @data(*[[1, 1, 1], [1, 1, 2], [1, 1, 3]])
        @data([1, 1, 1], [1, 1, 2], [1, 1, 3])
        def test_01(self, a):
            print(a)
    if __name__ == '__main__':
        unittest.main()
    

    unittest_13_参数化-基于ddt解包传递多个数据.py,使用unpack装饰器解包数据

    import unittest
    from ddt import ddt, data, unpack
    def add(a, b, c):
        return a + b + c
    class AddTest(unittest.TestCase):
        @data((1,2,3),(1,2,1),(1,3,1),(1,1,3))
        @unpack
        def test_01(self,a,b,c):
            add(a,b,c)
    if __name__ == '__main__':
        unittest.main()
    

    参考:https://www.cnblogs.com/Neeo/articles/11478853.html
    参考:https://www.cnblogs.com/Neeo/articles/11199127.html#通过smtp发邮件

  • 去QQ邮箱配置SMTP服务器和获取授权码
  • 编写测试用例
  • 生成测试报告
  • 使用第三方邮件服务发送测试报告
  • 单元测试框架-Pytest

    Pytest是基于Python语言的单元测试框架,也是一个命令行的工具,比 unittest 测试框架更灵活。具有以下特点:

  • 入门简单,易上手,官方文档丰富而且使用广泛,有大量的参数例子。
  • unittest有的,它都有,而且支持更为复杂的功能测试
  • 支持大量第三方插件,如:失败重试、控制用例执行顺序等。
  • 基于配置文件可以很简单的集成CI(持续集成)工具中。
  • pip install pytest
    # 测试是否安装成功
    pytest --version
    

    pytest_01_基本格式.py,代码:

    def add(x, y):
        return x + y
    class TestAddFunc(object):  # 测试用例类名必须用Test开头,若执行收集不到用例则将文件名改为test_开头试试
        def test_01(self):   # 方法名与函数名必须要用test_开头
            print(add(10, 20))
        def test_02(self):
            print(add("a", "B"))
        def test_03(self):
            print(add("a", 20))
    

    pytest提供了三种方式给测试人员执行测试用例:

    命令行运行

    pytest -s 文件名
    pytest -s -v 文件名
    # -s 输出测试用例的print语句打印的信息
    # -v 输出执行的测试用用例的类名以及方法名
    # -x 一旦发现失败用例,立即停止继续运行
    # -maxfail=2 当测试遇到2条失败用例,立即停止继续运行
    
    if __name__ == '__main__':
        # pytest.main() # 收集全部以test_开头的文件及其下面的类与方法
        # pytest.main(['test_BTDFSE.py::Test_BTDFSE', '-sv'])
        # os.system('pytest -s -v test_BTDFSE.py')
        # 先删除旧的测试报告
            shutil.rmtree("./allure_results")
            shutil.rmtree("./allure_reports")
        except:
        # 运行pytest执行测试测试用例,并生成json测试报告
        pytest.main(['test_BTDFSE.py::Test_BTDFSE', '-sv', '--alluredir', './allure_results'])
        # 基于os.system执行终端名,生成HTML格式文档的测试报告
        os.system("allure generate ./allure_results -o ./allure_reports --clean")
        # 基于http协议打开HTML测试报告
        os.system('allure open ./allure_reports')
    

    测试脚手架

    方法级别:setup与teardown

    类级别:setup_class与teardown_class,注意:这是实例方法,不是类方法

    模块级别:setup_module与teardown_module

    pytest_02_测试脚手架.py,代码:

    def add(x, y):
        return x + y
    def setup_module():
        print("模块执行初始化操作")
    def teardown_module():
        print("模块执行初始化putest")
    class TestAddFunc(object):  # 测试用例类名必须用Test开头
        def setup(self):
            print('setup执行初始化操作')
        def teardown(self):
            print('teardown执销毁操作')
        def setup_class(self):    # 注意:此处方法类型是实例方法。
            print('类级别:setup_class执行初始化操作')
        def teardown_class(self):  # 注意:此处方法类型是实例方法。
            print('类级别:teardown_class执行初始化操作')
        def test_01(self):   # 方法名与函数名必须要用test_开头
            print(add(10, 20))
        def test_02(self):
            print(add("a", "B"))
        def test_03(self):
            print(add(20, 20))
    

    基于配置文件运行pytest

    在pytest提供的终端运行测试用例的方式的基础上,pytest还支持使用配置文件来简化运行参数。

    可以通过pytest --help 查看pytest配置文件的名:pytest.ini、tox.ini、setup.cfg

    配置文件一般保存在项目根目录下。

    pytest.ini,配置文件格式:

    ; 命名空间,表示以下选项属于pytest配置
    [pytest]
    ; 运行参数
    addopts = -s -v
    ; 匹配搜索的测试文件的目录路径
    testpaths = ./
    ; 匹配搜索的测试文件名格式
    python_files = test_*.py
    ; 匹配搜索的测试类格式
    python_classes = Test*
    ; 匹配搜索的测试方法名格式
    python_functions = test_*
    ; markers
    markers = "测试标记-一般就是本轮测试的项目名或者模块"
    

    上面的注释,必须清除否则报错。有了配置文件以后,使用pytest命令即可运行测试用例。

    pytest
    

    Pytest的断言比unittest提供的断言更加简单易用,仅仅只需要使用assert关键字,后续跟上python原生的表达式即可。

    assert "m" in "moluo"
    assert "m" not in "moluo"
    assert 1 == 2
    assert 1 != 1
    assert 1 > 2
    assert not True
    assert type(1) is int
    assert type(1) not is int
    

    pytest_03_断言.py,代码:

    def add(x, y):
        return x + y
    class TestAddFunc(object):  # 测试用例类名必须用Test开头
        def test_01(self):   # 方法名与函数名必须要用test_开头
            res = add(10, 20)
            assert res == 30
        def test_02(self):
            res = add("a", "B")
            assert type(res) is int
        def test_03(self):
            res = add(20, 20)
            assert res != 20
    

    根据特定的条件,不执行标识的测试函数。

    @pytest.mark.skipif(判断条件, reason="跳过原因")
    

    pytest_04_跳过.py,代码:

    import pytest
    def add(x, y):
        return x + y
    version = (2, 7, 12)
    class TestAddFunc(object):  # 测试用例类名必须用Test开头
        def test_01(self):   # 方法名与函数名必须要用test_开头
            res = add(10, 20)
            assert res == 30
        @pytest.mark.skipif(version <= (2, 7, 12), reason="高于2.7以下,不测试test_02")
        def test_02(self):
            res = add("a", "B")
            assert type(res) is int
        def test_03(self):
            res = add(20, 20)
            assert res != 20
    

    pytest也支持参数化操作,而且不需要安装任何第三方模块即可使用,也不再需要ddt。

    import pytest
    def add(x, y):
        return x + y
    class TestAddFunc(object):  # 测试用例类名必须用Test开头
        @pytest.mark.parametrize("x,y", [(10, 20), {"x":10, "y":20}, ("a", "b"), ("a", 20)])
        def test_01(self, x, y):   # 方法名与函数名必须要用test_开头
            res = add(x, y)
            assert res == 30
    

    fixture-脚手架

    在Unittest中我们经常需要针对不同的测试用例使用脚手架完成一些测试的前置与后置操作,但是很多测试用例的前置与后置操作基本一样,所以pytest提供的fixture脚手架相比Unittest提供的脚手架进行了显著改进:

  • 有独立的命名,并通过声明它们从测试函数、模块、类或整个项目中的使用来激活脚手架。
  • 也可以按模块化的方式实现,每个fixture脚手架都可以在各个测试用例之间互相调用。
  • fixture的范围从简单的单元测试到复杂的功能测试,可以对fixture配置参数,或者跨函数function,类class,模块module或整个测试项目范围session。
  • pytest的fixture有个scope参数可以控制fixture的作用范围(从大到小):session>module>class>function。

    session:是多个文件调用一次,可以跨.py文件调用,每个.py文件就是module
    module:每一个.py文件调用一次,该文件内又有多个function和class
    class:每一个类调用一次,一个类中可以有多个方法
    function:每一个函数或方法都会调用
    
    实现参数化效果
    import pytest
    # 类级别的脚手架
    @pytest.fixture(scope="class")
    def fixture_01_data():  # 建议脚手架的函数名以fixture开头.
        a = 10
        b = 20
        print("脚手架运行了!")
        return a, b  # 脚手架的函数可以有返回值,也可以没有返回值
    # 被测试的代码单元
    def add(x, y):
        return x + y
    class TestAddFunc(object):
        def test_01(self, fixture_01_data):  # 此处的参数名,就是上面的脚手架名称,注意:参数名要与上面的脚手架函数保持一致
            print(f"fixture_01_data={fixture_01_data}")
            res = add(fixture_01_data[0], fixture_01_data[1])
            assert res == 30
    @pytest.mark.usefixtures("fixture_01_data")  #  此处参数为脚手架函数名
    @pytest.mark.usefixtures("fixture_02_data")
    class TestAddFunc(object):
        def test_01(self, fixture_01_data):
            res = add(fixture_01_data[0], fixture_01_data[1])
            assert res == 30
        def test_02(self, fixture_02_data):
            res = add(fixture_02_data[0], fixture_02_data[1])
            assert res == 30
    @pytest.fixture(scope="class", autouse=True)
    def fixture_01_data():
        print("fixture_01_data")
        a = 10
        b = 20
        return a, b
    @pytest.fixture(scope="class", autouse=True)
    def fixture_02_data():
        print("fixture_02_data")
        a = "10"
        b = "20"
        return a, b
    def add(x, y):
        return x + y
    @pytest.mark.usefixtures("fixture_01_data")  #  此处参数为脚手架函数名
    @pytest.mark.usefixtures("fixture_02_data")
    class TestAddFunc(object):
        def test_01(self, fixture_01_data):
            res = add(fixture_01_data[0], fixture_01_data[1])
            assert res == 30
        def test_02(self, fixture_02_data):
            res = add(fixture_02_data[0], fixture_02_data[1])
            assert res == 30
    
    使用yield实现setup/teardown效果

    pytest的fixture不仅可以在单元测试、集成测试中被广泛使用,甚至在UI测试或系统测试中也会经常用到,针对UI测试或系统测试中的功能测流或流程测试,总是经常需要打开相关环境,例如:web项目进行UI测试,每次都会需要打开浏览器或关闭浏览器等前置或后置的操作的。

    import pytest
    @pytest.fixture(scope="class", autouse=True)
    def fixture_open_browser():
        print("打开浏览器")  # 相当于setup
        yield "xiaoming", "123456"
        # 生成器函数中的暂停关键字,作用是当代码运行到yield时,把yield右边的数据作为返回值提供给调用处,把代码执行权交出去。
        print("关闭浏览器")  # 相当于teardown
    class TestUser(object): # object是一个基类,python中所有的类都是默认继承于object的。
        def test_01(self, fixture_open_browser):
            print(f"fixture_open_browser={fixture_open_browser}")
            print("注册流程,测试用户是否能注册成功")
        def test_02(self, fixture_open_browser):
            print(f"fixture_open_browser={fixture_open_browser}")
            print("登陆流程,测试用户是否能登陆成功")
    
    单独存放fixture代码

    我们还可以基于fixture脚手架把代码提前写好,放在在一个pytest能自动识别的conftest.py文件中,这样可以有效避免出现重复的fixture代码。注意:conftest.py的文件名必须固定,而且里面只存放fixture代码,并保证该文件与被测试代码文件在同一目录即可。

    conftest.py,代码:

    import pytest
    @pytest.fixture(scope="class", autouse=True)
    def fixture_open_browser():
        print("打开浏览器")  # 相当于setup
        yield "xiaoming", "123456"
        # 生成器函数中的暂停关键字,作用是当代码运行到yield时,把yield右边的数据作为返回值提供给调用处,把代码执行权交出去。
        print("关闭浏览器")  # 相当于teardown
    
    class TestUser(object):
        def test_01(self, fixture_open_browser):
            print(f"fixture_open_browser={fixture_open_browser}")
            print("注册流程,测试用户是否能注册成功")
        def test_02(self, fixture_open_browser):
            print(f"fixture_open_browser={fixture_open_browser}")
            print("登陆流程,测试用户是否能登陆成功")
    

    第三方常用组件

    控制测试用例执行顺序

    unittest执行测试用例的默认顺序是根据测试用例方法名的ASCII码排序[0-9A-Za-z]而定的,值越小,越靠前执行。

    pytest执行测试用例的默认顺序是根据测试方法的源代码上下顺序来排序的。

    而如果在完成接口测试或集成测试时,我们要控制测试用例的执行顺序,可以通过pytest的第三方模块pytest-ordering来实现。

    pip install pytest-ordering
    
    class TestAdd(object):
        @pytest.mark.run(order=n)  # n表示执行顺序,可以是正负整数。
        def test_测试方法名(self):
    # 执行顺序为优先执行正数排序的方法,接着到没有排序的方法,最后是负数排序的方法。
    # 如果多个方法都是正数,则先执行排序值小的,同理如果多个方法都是负数,也是一样先执行排序值小的。
    

    pytest-ordering.py组件的使用,代码:

    前置步骤,安装插件: pip install pytest-ordering import pytest class TestAdd(object): @pytest.mark.run(order=-1) def test_01(self): print(f"test_01执行了,order=-1") @pytest.mark.run(order=-10) def test_02(self): print(f"test_02执行了,order=-10") @pytest.mark.run(order=10) def test_03(self): print(f"test_03执行了,order=10") @pytest.mark.run(order=3) def test_04(self): print(f"test_04执行了,order=3") def test_05(self): print(f"test_05执行了,没有指定排序值") def test_06(self): print(f"test_06执行了,没有指定排序值") 多个方法排序值为正整数的情况:以小为先 test_04 test_03 没有排序值的情况下,源代码中先写的先执行,后写的后执行:先写为先 test_05 test_06 多个方法排序值为负整数的情况:以小为先 test_02 test_01

    pytest-ordering用于解决测试开发中,UI测试、系统测试、接口测试等多个测试用例有先后流程的场景。

    例如,系统测试的购物车流程:

  • 先访问商品详情页
  • 添加商品购物车
  • 点击购物车的结算
  • 填写或选择发货地址
  • 注意:pytest-ordering组件如果和参数化fixtrue脚手架一起使用会导致参数解析错误,所以不能一起使用。因此pytest-ordering使用时,如果需要对测试用例实现参数化,则可以使用pytest.mark.parametrize,注意:不能使用ddt。

    失败用例重试

    针对网络场景或服务端性能不稳定的情况下,进行测试时经常遇到用例运行失败的情况,特别在性能测试方面,此时我们可以让失败用例重试指定次数,以达到测试的更准确的结果。

    pip install pytest-rerunfailures -i http://pypi.douban.com/simple/
    # pip freeze | findstr rerunfailures # windows系统
    # pip freeze | grep rerunfailures    # mac OS X 或 linux、Windows10以上
    
    安装 pytest-rerunfailures 插件到本地以后,在pytest运行参数中会新增选项:
    --reruns n (n为正整数,表示重试n次)
    --reruns-delay m (m为正整数,表示重试的间隔时间为m秒)
    
    全局失败用例重试

    全局失败用例重试.py,代码

    import random
    def add(x, y):
        return x + y
    class TestAdd(object):
        def test_01(self):
            res = add(10, 20)
            assert res is 30
        def test_02(self):
            ret = random.randint(1, 3)
            assert ret % 2 == 0
    
  • 局部重试参数会覆盖全局重试参数,也就是说,当使用了局部用法,全局用法就失效了。
  • 与pytest.fixture脚手架也会存在参数解释冲突问题,所以使用了失败重试就不要使用pytest.fixture。
  • 并发运行测试用例

    当测试用例非常多的时候,一条条按顺序执行测试用例,是很浪费测试时间的。此时如果测试用例之间没有先后运行的依赖关系,可以完全独立运行的情况下,我们也可以并发运行测试用例,让自动化测试用例可以分布式执行,从而大大节省测试时间。pytest-xdist 就可以完成我们上面希望的并发执行测试用例的效果,它是属于进程级别的并发。

    pip install pytest-xdist
    

    pytest-xdist安装以后,pytest会新增一个参数-n,可以让我们指定本次运行测试所开启的进程数量。

    参数设置如下:

    pytest -s -v -n 4  # 使用4个进程运行,也可以改成auto
    pytest -s -v -n auto  # 自动检测系统的CPU核数,并根据CPU核算创建对应数量的进程数量
    

    pytest.ini,代码:

    [pytest]
    addopts =-s -v -n auto
    testpaths = ./
    python_files = test_*.py
    python_classes = Test*
    python_functions = test_*
    
    def add(x, y):
        return x + y
    class TestAdd(object):
        def test_01(self):
            res = add(10, 20)
            assert res is 30
        def test_02(self):
            res = add("10", "20")
            assert res == "1020"
        def test_03(self):
            res = add("10", "20")
            assert res == "1020"
        def test_04(self):
            res = add("10", "20")
            assert res == "1020"
        def test_05(self):
            res = add("10", "20")
            assert res == "1020"
        def test_06(self):
            res = add("10", "20")
            assert res == "1020"
    

    自身生成HTML格式测试报告[了解下即可]

    pip install pytest-html
    
    安装插件到本地以后,在pytest运行参数中会新增选项:--html=report.html
    

    生成HTML格式测试报告.py,代码

    import random
    import pytest
    def add(x, y):
        return x + y
    class TestAdd(object):
        def test_01(self):
            res = add(10, 20)
            assert res is 30
        # 只设置当前测试用例方法失败重试
        @pytest.mark.flaky(reruns=3, reruns_delay=2)
        def test_02(self):
            ret = random.randint(1, 3)
            assert ret % 2 == 0
    

    pytest.ini,代码:

    [pytest]
    addopts =-s -v --html=report.html
    testpaths = ./
    python_files = test_*.py
    python_classes = Test*
    python_functions = test_*
    

    Allure

    Allure 是一款轻量级的开源自动化测试报告生成框架,Java语言开发出来的。它支持绝大部分测试框架,比如 pytest、unittest 等。比起上面那些丑陋的测试报告生成,Allure是最漂亮的,而且还可以配合pytest与Jenkins实现CI持续集成。pytest+Allure+git+pycharm+Jenkins+gitlab/gitee/github= CI持续集成

    官方文档:https://docs.qameta.io/allure/#_get_started

    allure依赖Java环境,你要安装Java 环境:https://www.cnblogs.com/Neeo/articles/11954283.html

    allure插件下载:百度云盘链接:链接:https://pan.baidu.com/s/1Xj1A_xsRscOZHskTR4xjAg 提取码:6b33(已保存到本地百度网盘)

    或者去https://github.com/allure-framework/allure2/releases下载

    allure插件安装:
    解压压缩包到一个没有中文的目录中,然后将其中的bin目录添加到环境变量(PATH)中

    测试,终端输入:

    安装java环境

    Allure的运行起来需要依赖于java环境,此处下载安装jdk1.8环境(Java 8)。

    java的jdk1.8下载地址:https://www.java.com/zh-CN/download/

    此处我们演示windows下的java环境安装。

    鼠标右键点选下载到本地的java的jdk安装包,选择以"管理员身份运行",窗口如下,勾选窗口左下角的"
    更改目标文件夹",点击"安装",进入下一步。

    默认情况下,java的jdk会选择在C盘的"Program Files"目录下安装,如果不想要在此目录安装,则可以点击更改,如果无所谓,则点击"下一步"即可。建议修改“Program Files”之前的路径,后半段保持原样。

    jre与jdk同样保存在一个父目录下即可。

    OK,接下来,喝杯茶,等待一会即可。

    效果如下,则没有问题。

    注意:javasdk环境是依赖于JAVA_HOME的,依次打开控制面板系统与安全系统高级系统设置环境变量系统变量新建。变量名中输入JAVA_HOME,变量值中填写刚才获取到的路径C:\tool\Java\jdk1.8.0_201(注意,此处根据自己的实际路径填写,别瞎复制)。

    保存了JAVA_HOME环境变量,点击下方的Path环境变量,里面把java sdk安装目录下bin目录加入到环境变量中。

    cmd终端输入java -version,出现如下内容表示安装完成。

    安装Allure命令行工具

    # windows10系统打开powershell来安装,不能使用cmd
    # 安装Allure可以借助第三方包管理器scoop,scoop用于提供给开发人员安装各种开发软件到windows系统的
    Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
    iwr -useb https://gitee.com/RubyKids/scoop-cn/raw/master/install.ps1 | iex
    # https://scoop.sh/#/apps?q=allure&s=0&d=1&o=true
    scoop bucket add main
    scoop install allure
    # # Ubuntu系统使用apt-get包管理器
    # sudo apt install -y openjdk-8-jdk
    # sudo apt-add-repository ppa:qameta/allure
    # sudo apt-get update 
    # sudo apt-get install allure
    # # Mac OS X系统使用brew包管理器
    # brew install openjdk
    # brew install allure
    

    接下来,我们重新打开一个新的命令行窗口,输入"allure",显示如下即表示allure命令行工具安装正确。

    安装pytest-allure集成模块

    接下来,只需要安装allure集成到pytest的pytest-allure集成模块即可。

    pip install allure-pytest
    

    生成allure测试结果,结果以json文件格式保存在--alluredir选项指定的目录下。

    pytest.ini,代码:

    [pytest]
    addopts =-s -v --alluredir=./allure_results
    testpaths = ./
    python_files = test_*.py
    python_classes = Test*
    python_functions = test_*
    

    基于json报告结果使用浏览器展示allure测试结果报告

    allure serve ./allure_results
    

    生成HTML格式文档的测试报告

    HTML格式文档的测试报告也是要基于上面的json文件格式的测试结果才能生成的。

    # 必须先生成json格式的测试报告,才能生成HTML格式的测试报告
    allure generate ./allure_results -o ./reports --clean
    @allure.severity(severity_level)
    设置测试用例的优先级,allure对测试用例的优先级划分成五个等级(allure.severity_level),值为:
    BLOCKER(阻塞缺陷),CRITICAL(严重缺陷),NORMAL(普通缺陷),MINOR(较小缺陷),TRIVIAL(不重要缺陷)。 默认值为NORMAL。 @allure.title(test_title) 设置测试用例的标题名称 @allure.description(test_description) 设置测试用例的详细描述(纯文本) @allure.description_html(test_description_html) 设置测试用例的详细描述(HTML文本) @allure.link(url, link_type=LinkType.LINK, name=None) 设置指定测试用例的缺陷访问地址 allure.step(title) 设置测试用例执行过程中的步骤信息 allure.attach(body, name=None, attachment_type=None, extension=None) 设置测试用例的文本附件信息,allure.attachment_type的常见文本附件类型支持:TEXT,SVG,HTML,JSON,YAML,CSV。
    添加文本格式:allure.attach("HTML代码", "文件标题", allure.attachment_type.HTML) allure.attach.file(source, name=None, attachment_type=None, extension=None) 设置测试用例的文件附件信息,支持的类型参考上方使用。
    allure.attachment_type的常见文本附件类型支持:PNG,GIF,JPG,PDF。添加二进制格式:allure.attach.file("文件路径", "文件名", attachment_type=allure.attachment_type.PNG) allure.dynamic.title(item['title']) 在参数化的时候,我们无法使用allure的相关功能,因为装饰器中是写死的功能,在参数中,数据都是在用例中使用。所以,我们采用allure.dynamic参数动态的为用例添加相关的功能。
    import requests
    import pytest
    import allure
    url_list = [
        {'url': "https://www.baidu.com", "method": "get", "status": 200, "title": "百度"},
        {'url': "https://www.cnblogs.com/Neeo/articles/11832655.html", "method": "get", "status": 200, "title":"cnblogs"},
        {'url': "http://www.neeo.cc:6001/post", "method": "post", "status": 200, "title":"post接口"},
        {'url': "http://www.neeo.cc:6001/put", "method": "put", "status": 200, "title":"put接口"},
    @pytest.mark.parametrize('item', url_list)
    def test_case(item):
        allure.dynamic.title(item['title'])
        response = requests.request(method=item['method'], url=item['url'])
        # print(response.status_code,  item['status'])
        assert response.status_code == item['status']
    @allure.testcase("http://www.luffycity.com", "测试地址站点的首页地址")
    @allure.issue("http://www.luffycity.com", "软件缺陷的管理站点的首页地址")
    class TestAdd(object):
        @allure.feature("测试用例模块的描述: 购物车模块,用户模块")
        @allure.story("测试用例的分类描述")
        @allure.title("测试用例test_01的标题描述")
        @allure.description("测试用例test_01的详细描述")
        @allure.severity(allure.severity_level.MINOR)  # 较小缺陷等级的用例,如果不设置这个,默认是NORMAL普通缺陷等级的用例
        def test_01(self):
            res = add(10, 20)
            assert res == 30
        @allure.feature("测试用例模块的描述: 购物车模块,用户模块")
        @allure.story("测试用例的分类描述")
        @allure.title("测试用例test_02的标题描述")
        # @allure.description("测试用例test_02的详细描述")    # 纯文本描述
        @allure.description_html("<b style='color: red;'>测试用例test_02的详细描述</b>")  # HTML文本描述
        @allure.severity(allure.severity_level.BLOCKER)  # 阻塞缺陷等级的用例
        @allure.link("http://test.luffycity.com/test_02", link_type=LinkType.LINK, name="测试用例:02")
        # 只设置当前测试用例方法失败重试
        @pytest.mark.flaky(reruns=3, reruns_delay=2)
        def test_02(self):
            # 注意是使用with上下文管理器语句
            with allure.step("步骤1:内容描述"):
                result = 1+1
            with allure.step("步骤2:内容描述"):
                result +=1
            print(f"测试结果是:{result}")
            assert result == 3
        @allure.feature("测试用例模块的描述: 购物车模块,用户模块")
        @allure.story("测试用例的分类2描述")
        @allure.title("测试用例test_03的标题描述")
        @allure.description("测试用例test_03的详细描述")
        @allure.severity(allure.severity_level.CRITICAL)  # 致命缺陷等级的用例
        def test_03(self):
            # 图片附件
            allure.attach.file("./images/demo.jpg", 'demo.jpg', allure.attachment_type.JPG)
            # 文本附件
            allure.attach("""<h1>Test with some complicated html description</h1>
    <table style="width:100%">
        <th>Firstname</th>
        <th>Lastname</th>
        <th>Age</th>
      <tr align="center">
        <td>William</td>
        <td>Smith</td>
        <td>50</td>
      <tr align="center">
        <td>Vasya</td>
        <td>Jackson</td>
        <td>94</td>
    </table>""", "Test with some complicated html attachment", allure.attachment_type.HTML)
            ret = random.randint(1, 3)
            assert ret % 2 == 0
    # __name__ 是一个魔术变量,在当前文本被python解释器作为主程序运行是,值固定就是 "__main__",
    # 如果当前文件作为模块被其他文件导包使用,则__name__的值,则为当前文件名或者其他的自定义名称,总之不是 "__main__"了。
    if __name__ == '__main__':
            # 删除之前的测试结果与测试文件目录内容
            shutil.rmtree("reports")
            shutil.rmtree("results")
        except:
        pytest.main(["-sv", "main.py", "--alluredir", "./results"])
        # 生成报告html文件
        os.system('allure generate ./results -o ./reports --clean')
        # 基于http协议打开HTML测试报告
        os.system('allure open ./reports')
    

    测试运行,使用终端

    python main.py
    

    补充:关于在python中可以执行命令行命令

    settings.py

    # -------------------- allure 报告相关 ------------
    ALLURE_COMMAND = 'allure generate {from_json_path} -o {save_to_path} --clean'.format(
        from_json_path=os.path.join(BASE_DIR, 'report', 'json_result'),
        save_to_path=os.path.join(BASE_DIR, 'report', "allure_result")
    
    import subprocess
    # 参考: https://docs.python.org/zh-cn/3/library/subprocess.html
    subprocess.call(settings.ALLURE_COMMAND, shell=True)
    

    如何删除一个非空目录

    用shuitil

    import shutil
    shutil模块对文件和文件集合提供了许多高级操作。特别是,提供了支持文件复制和删除的功能
    # shutil.copy(src, dst)   # 拷贝文件
    # shutil.move(src, dst)  # 移动目录或者文件
    # shutil.rmtree(path)   # 递归删除目录,无法直接删除文件
    # shutil.make_archive(base_name, format('zip'))   # 将目录或者文件以指定格式压缩
    # shutil.unpack_archive(filename, extract_dir)  # 解压缩
    # see also: https://docs.python.org/3/library/shutil.html
        pytest.main()
        # 基于os.system执行终端名,生成HTML格式文档的测试报告
        os.system("allure generate ./allure_results -o ./allure_reports --clean")
    
    import pytest
    import os
    import sys
    import config
    if __name__ == '__main__':
        print('开始删除俩个文件')
        os.system(f"rd /s /q allure_results")
        os.system(f"rd /s /q allure_reports")
        # 让python解释器,追加3个项目中的核心目录为导包路径
        sys.path.insert(0, str(config.BASE_DIR / "api"))
        sys.path.insert(0, str(config.BASE_DIR / "tests"))
        sys.path.insert(0, str(config.BASE_DIR / "utils"))
        # 启动pytest框架
        pytest.main()
        # 生成报告html文件
        os.system('allure generate ./allure_results -o ./allure_reports')
        # 基于http协议打开HTML测试报告
        os.system(f'allure open ./reports -h {config.HOST} -p {config.PORT}')
    

    接口自动化测试

    基于pytest和allure构建接口自动化测试框架构与项目【luffytest项目】。

    接口自动化的框架开发:

  • 用到的知识点:
  • pytest
  • allure
  • Excel操作,不会,用xlrd
  • 日志操作,学过,不太会
  • 文件操作,文件压缩, 没讲,但你要会的,zipfile
  • 执行终端命令,os.system, subprocess:cell, popen
  • 如何使用python查看当前目录下的所有文件或者目录?
  • 读取Excel,每一行数据都是一个用例,你在读出来之后,把这个一行用例封装成一个对象,字典,列表。
  • 使用参数化每次讲一个用例对象传进去。
  • 使用requests获取用例对象中的相关参数发请求。
  • 然后将请求结果与预期值(用例对象)做断言
  • 此时,allure所需的json数据已经有了。
  • 使用allure命名读取josn数据生成测试报告
  • 将报告压缩
  • 使用发邮件功能将压缩文件发送
  • 在重点位置,添加日志功能
  • 框架目录结构

    我们要构建一个自动化测试框架,就要以项目的概念来对项目中所有的代码文件进行划分目录和文件结构,不同的代码功能不一样,所以我们需要设计一个合理的目录结构,以方便与测试开发团队的其他人员进行测试功能的开发与测试,也方便将来的项目代码维护。

    ├─ config.py # 项目代码配置文件 ├─ pytest.ini # pytest模块配置文件 ├─ main.py # 主程序,执行入口 ├─ api/ # 封装被测试项目的api接口存放目录[用于mock测试、冒烟测试] ├─ data/ # 测试数据/测试用例的存放目录 ├─ allure_results/ # 测试报告结果生成目录 ├─ allure_reports/ # HTML测试报告生成目录 ├─ tests/ # 测试用例脚本存放目录 ├─ libs/ # 第三方工具类的存放目录[开源模块,不是当前项目封装的模块] └─ utils/ # 自定义工具类的存放目录[当前项目自己封装的模块]

    配置文件,config.py,代码:

    import pathlib  # 路径操作模块,替代 os.path模块,os.path采用字符串来操作路径,pathlib采用面向对象来操作路径
    # 项目目录的主目录路径[字符串路徑]
    BASE_DIR_STR = pathlib.Path(__file__).parent.resolve().as_posix()  # 基本操作系统转换路径的分隔符 as_posix
    # 項目目录的主目录路径[路径对象]
    BASE_DIR = pathlib.Path(BASE_DIR_STR)
    # 项目名
    WEB_NAME = "路飞自动化接口测试框架"
    # 测试自动化项目的运行端口与IP地址
    HOST = "127.0.0.1"
    PORT = 8088
    # print(pathlib.Path(__file__))  # D:\data\1045699\Desktop\luffy_code\testting\luffytest\config.py
    # print(BASE_DIR_STR, type(BASE_DIR_STR))  # D:/data/1045699/Desktop/luffy_code/testting/luffytest   <class 'str'>
    # print(BASE_DIR, type(BASE_DIR))  # D:\data\1045699\Desktop\luffy_code\testting\luffytest   <class 'pathlib.WindowsPath'>
    

    入口文件,main.py,代码:

    import pytest
    import os
    import sys
    import config
    if __name__ == '__main__':
        print('开始删除俩个文件')
        os.system(f"rd /s /q allure_results")
        os.system(f"rd /s /q allure_reports")
        # 让python解释器,追加3个项目中的核心目录为导包路径
        sys.path.insert(0, str(config.BASE_DIR / "api"))
        sys.path.insert(0, str(config.BASE_DIR / "tests"))
        sys.path.insert(0, str(config.BASE_DIR / "utils"))
        # 启动pytest框架
        # pytest.main()   # 此方法不可靠,执行时中间夹杂别的py文件导致allure生成不了json数据
        os.system('pytest -s -v --alluredir=./allure_results --clean-alluredir')
        # 生成报告html文件
        os.system('allure generate ./allure_results -o ./allure_reports')
        # 基于http协议打开HTML测试报告
        # os.system(f'allure open ./allure_reports -h {config.HOST} -p {config.PORT}')
    

    OK,完成了上面操作以后,我们就可以写一个测试用例来测试下现在我们的基本框架是否能正常运行了。

    tests/users/test_login.py,代码:

    import allure
    import config
    @allure.epic(config.WEB_NAME)
    @allure.feature("用户模块")
    @allure.story("登录")
    class TestLogin(object):
        @allure.severity(allure.severity_level.CRITICAL)
        def test_username_by_empty(self,):
            allure.dynamic.title("用户名为空,登陆失败")
            allure.dynamic.description("测试用户名为空的登陆结果")
            allure.attach('附件内容', '附件名字')
        def test_password_by_empty(self,):
            allure.dynamic.title("密码为空,登陆失败")
            allure.dynamic.description("测试密码为空的登陆结果")
            allure.attach('附件内容', '附件名字')
    

    运行框架,效果:

    当然自动化接口框架的构建开发过程肯定不是一夜之间就能完成的,所以我们需要长期构建,不断完善的准备。所以需要使用git代码版本管理工具把代码推送到git仓库中进行代码版本的管理(在企业中一般我们会使用公司内部构建的gitlab平台来管理内部项目,但现在我们处于学习阶段,所以可以先把代码提交到gitee码云上)。

    注意:公司内部的代码不要私自自己往gitee(码云),github去推。

    gitee官网地址:https://gitee.com/

    需要提前在当前开发机子上安装git代码版本管理工具。

    windows下载地址:https://git-scm.com/

    我们是学习,所以我这创建项目库名luffytest。项目库名建议是英文的。

    选择git-flow自定义分支模型。

    所谓的分支,其实就是一个项目的代码的不同流程版本。

    git-flow分支命名规范:

    补充说明:

    release/feature/xxx   # 表示待发布分支下的xxx业务功能相关代码
    develop/feature/xxx   # 表示开发分支下的xxx业务的新功能代码
    develop/test/xxx      # 表示开发分支下的正在测试的xxx业务功能的代码
    develop/hotfix/xxx    # 表示开发分支下的修复xxx业务相关功能的bug
    # 当然,分支名称并不是固定的,只要能让开发人员一目了然,名字都可以换,所以也可以这样:
    release/xxx   # 表示待发布的xxx业务新功能代码
    feature/xxx   # 表示正在开发的xxx业务功能代码
    test/xxx      # 表示正在测试的xxx业务功能代码
    hotfix/user   # 表示正在修复bug的xxx业务功能的代码
    

    创建私有空仓库以后的界面:

    免密提交代码

    接下来,我们可以使用ssh连接远程的git仓库,需要先在本地电脑下生成ssh秘钥对。

    # 例如我的码云账号是 [email protected],那么该账号生成ssh秘钥对(基于rsa加密算法生成的秘钥对,公钥和私钥)
    ssh-keygen -t rsa -C "[email protected]"
    

    复制终端下出现的公钥信息,复制到码云上面。

    # 查看生成的秘钥对中的公钥(私钥id_rsa保存在自己电脑不要乱动,公钥id_rsa.pub需要复制文件内容到码云平台)
    cat C:\Users\Administrator/.ssh/id_rsa.pub
    # 把公钥进行复制到码云平台上 https://gitee.com/profile/sshkeys
    

    切换项目的仓库地址,设置线上仓库

    git config --global user.name "mooluo"
    git config --global user.email "[email protected]"
    # 在项目根目录下初始化git仓库
    cd luffytest/  # 具体的路径根据自己的设置而定
    git init
    # git remote remove origin # 删除仓库地址,origin可以理解是一个变量,因为当前时一个新仓库,所以不需要执行这段。
    git remote add origin [email protected]:mooluo_admin/luffytest.git # 新仓库地址,等同于origin= git..../luffycity.git
    

    分支管理,git提交代码版本并同步到远程服务器。

    git branch    # 查看分支
    # 刚执行git init 初始化时,会没有分支,因此我们需要进行第一次的代码提交。
    git add .
    git commit -m "feature: 项目初始化"
    # 经过上面的命令操作,本地的git就会自动生成一个master分支
    # git branch <分支名称>    # 新建分支
    # git branch test         # 例如:创建一个test分支
    # git checkout <分支名称>  # 切换分支
    # git checkout test       # 例如:切换到test分支,检出分支代码
    # git branch -d <分支名称>   # 删除分支
    # git branch -d test
    # git push <远程仓库别名> --delete <分支名称>    # 删除远程服务器分支
    # git push origin --delete test  # 例如:删除远程仓库origin中的test
    # 推送代码记录到远程服务器的代码仓库
    git push origin master # 推送的过程中,如果本地有该分支,但是线上没有这个分支,则git会自动在远程中创建该分支,默认的空仓库是一个分支都没有的。
    

    使用.gitignore可以在git上传或下载代码时,把一些不必要记录的垃圾文件/目录过滤掉。

    注意:必须保证.git目录 和.gitignore在同一级目录下,才能生效。

    .gitignore编写参考地址:https://github.com/github/gitignore

    .gitignore文件内容:

    # Byte-compiled / optimized / DLL files
    __pycache__/
    *.py[cod]
    *$py.class
    # C extensions
    # Distribution / packaging
    .Python
    build/
    develop-eggs/
    dist/
    downloads/
    eggs/
    .eggs/
    lib64/
    parts/
    sdist/
    wheels/
    share/python-wheels/
    *.egg-info/
    .installed.cfg
    *.egg
    MANIFEST
    # PyInstaller
    #  Usually these files are written by a python script from a template
    #  before PyInstaller builds the exe, so as to inject date/other infos into it.
    *.manifest
    *.spec
    # Installer logs
    pip-log.txt
    pip-delete-this-directory.txt
    # Unit test / coverage reports
    htmlcov/
    .tox/
    .nox/
    .coverage
    .coverage.*
    .cache
    nosetests.xml
    coverage.xml
    *.cover
    *.py,cover
    .hypothesis/
    .pytest_cache/
    cover/
    # Translations
    *.pot
    # Django stuff:
    *.log
    local_settings.py
    db.sqlite3
    db.sqlite3-journal
    # Flask stuff:
    instance/
    .webassets-cache
    # Scrapy stuff:
    .scrapy
    # Sphinx documentation
    docs/_build/
    # PyBuilder
    .pybuilder/
    target/
    # Jupyter Notebook
    .ipynb_checkpoints
    # IPython
    profile_default/
    ipython_config.py
    # pyenv
    #   For a library or package, you might want to ignore these files since the code is
    #   intended to run in multiple environments; otherwise, check them in:
    # .python-version
    # pipenv
    #   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
    #   However, in case of collaboration, if having platform-specific dependencies or dependencies
    #   having no cross-platform support, pipenv may install dependencies that don't work, or not
    #   install all needed dependencies.
    #Pipfile.lock
    # poetry
    #   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
    #   This is especially recommended for binary packages to ensure reproducibility, and is more
    #   commonly ignored for libraries.
    #   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
    #poetry.lock
    # PEP 582; used by e.g. github.com/David-OConnor/pyflow
    __pypackages__/
    # Celery stuff
    celerybeat-schedule
    celerybeat.pid
    # SageMath parsed files
    *.sage.py
    # Environments
    .venv
    venv/
    env.bak/
    venv.bak/
    # Spyder project settings
    .spyderproject
    .spyproject
    # Rope project settings
    .ropeproject
    # mkdocs documentation
    /site
    # mypy
    .mypy_cache/
    .dmypy.json
    dmypy.json
    # Pyre type checker
    .pyre/
    # pytype static type analyzer
    .pytype/
    # Cython debug symbols
    cython_debug/
    # PyCharm
    #  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
    #  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
    #  and can be added to the global gitignore or merged into this file.  For a more nuclear
    #  option (not recommended) you can uncomment the following to ignore the entire idea folder.
    .idea/
    # vue https://github.com/vuejs/vue/blob/dev/.gitignore
    .DS_Store
    node_modules
    *.log
    explorations
    TODOs.md
    dist/*.gz
    dist/*.map
    dist/vue.common.min.js
    test/e2e/reports
    test/e2e/screenshots
    coverage
    RELEASE_NOTE*.md
    dist/*.js
    packages/vue-server-renderer/basic.js
    packages/vue-server-renderer/build.js
    packages/vue-server-renderer/server-plugin.js
    packages/vue-server-renderer/client-plugin.js
    packages/vue-template-compiler/build.js
    .vscode
    # luffytest
    reports/*
    results/*
    logs/*
    

    记录并保存.gitignore到git中

    git add .
    git commit -m "feature: 新建.gitignore忽略文件"
    # 推送代码记录到远程服务器的代码仓库
    git push origin master
    

    git commit 提交版本的描述信息,编写前缀规范:

    最终,成功提交了代码版本到gitee平台。

    新增日志功能

    针对的项目开发,将来肯定需要把测试框架这个项目保存公司的服务器的,所以如果项目在公司服务器报错了,我们有可能不在场,或者其他同时去运作,那么我们针对当前这个项目在这个运行期间,有没有出现异常,那就需要记录整个项目的运行信息。

    参考:https://www.cnblogs.com/Neeo/articles/10951734.html#示例

    config.py,新增如下日志配置代码,代码:

    import pathlib  # 路径操作模块,替代 os.path模块,os.path采用字符串来操作路径,pathlib采用面向对象来操作路径
    # 项目目录的主目录路径[字符串路徑]
    BASE_DIR_STR = pathlib.Path(__file__).parent.resolve().as_posix()  # 基本操作系统转换路径的分隔符 as_posix
    # 項目目录的主目录路径[路径对象]
    BASE_DIR = pathlib.Path(BASE_DIR_STR)
    # 项目名
    WEB_NAME = "路飞自动化接口测试框架-master"
    # 测试自动化项目的运行端口与IP地址
    HOST = "127.0.0.1"
    PORT = 8088
    """日志配置"""
    LOGGING = {
        "name": "luffytest",  # 日志处理器的名称,一般使用当前项目名作为名称
        "filename": (BASE_DIR / "logs/luffytest.log").as_posix(),  # 日志文件存储路径,注意,一定要在项目根目录下手动创建logs目录
        "charset": "utf-8",  # 日志内容的编码格式
        "backup_count": 31,  # 日志文件的备份数量
        "when": "d",   # 日志文件的创建间隔事件,m 表示每分钟创建1个,h表示每小时创建1个,d表示每天创建1个,m0~m6表示每周星期日~星期六创建1个,midnight表示每日凌晨
        def __init__(self, name=None, filename=None):
            self.name = config.LOGGING.get("name", "pytest")
            if name:
                self.name = name
            self.filename = config.LOGGING.get("filename", None)
            if filename:
                self.filename = filename
            self.charset = config.LOGGING.get("charset", "utf-8")
            self.log_backup_count = config.LOGGING.get("backup_count", 31)
            self.when = config.LOGGING.get("when", "d")
        def get_logger(self):
            # 创建logger,如果参数name表示日志器对象名,name为空则返回root logger
            logger = logging.getLogger(self.name)
            # 务必设置一个初始化的日志等级
            logger.setLevel(logging.DEBUG)
            # 这里进行判断,如果logger.handlers列表为空则添加,否则多次调用log日志函数会重复添加
            if not logger.handlers:
                # 创建handler
                fh = logging.handlers.TimedRotatingFileHandler(
                    filename=self.filename,
                    when=self.when,
                    backupCount=self.log_backup_count,
                    encoding=self.charset
                sh = logging.StreamHandler()
                # 单独设置logger日志等级
                fh.setLevel(logging.INFO)
                # 设置输出日志格式
                simple_formatter = logging.Formatter(
                    fmt="{levelname} {name} {module}:{lineno} {message}",
                    style="{"
                verbose_formatter = logging.Formatter(
                    fmt="{levelname} {asctime} {name} {pathname}:{lineno} {message}",
                    datefmt="%Y/%m/%d %H:%M:%S",
                    style="{"
                # 为handler指定输出格式
                fh.setFormatter(verbose_formatter)
                sh.setFormatter(simple_formatter)
                # 为logger添加的日志处理器
                logger.addHandler(fh)
                logger.addHandler(sh)
            return logger  # 直接返回logger
    if __name__ == '__main__':
        from datetime import datetime
        log = LogHandle()
        log.filename = "../logs/pytest.log"
        logger = log.get_logger()
        logger.debug("日期测试")
        logger.info("普通日志")
        logger.warning("警告日志")
        logger.error("错误日志")
        logger.fatal("致命错误信息")
    class TestLogin(object):
        def test_username_by_empty(self,):
            logger = LogHandle().get_logger()
            logger.debug("debug")
            logger.info("运行日志-info")
            logger.warning("warning")
            logger.error("error")
            logger.critical("critical")
            allure.attach('附件内容', '附件名字')
        def test_login(self,):
            allure.attach('附件内容', '附件名字')
    

    提交代码版本,代码:

    git add .
    git commit -m "feature: 新增日志功能"
    # 推送代码记录到远程服务器的代码仓库
    git push origin master
    

    实现接口自定义测试

    封装请求工具requests

    接口测试肯定离不开http网络请求,在工作中python开发常用的http网络请求模块有:urllib,requests、httpx与aiohttp。

    其中我们作为测试开发,比较常用的就是requests模块了。

    Requests 是一个⽤Python编写,基于urllib的开源HTTP网络请求工具第三方库。它⽐python内置的urllib模块使用更加简单⽅便,可以节约我们⼤量的⼯作,完全满⾜HTTP测试需求。

    requests快速使用
    pip install requests
    

    requests.request(method, url, **kwargs)类能够构造一个请求,支持不同的请求方式。

    import requests
    response = requests.request(method='get', url='https://www.baidu.com')
    print(response.status_code)
    

    request类中来看看几个参数:

  • method:请求方式。
  • url:请求URL。
  • kwargs:
  • params:字典或者字节序列,作为参数增加到url中,使用这个参数可以把一些键值对以k1=v1&k2=v2的模式增加到url中,get请求中用的较多。
  • data:字典、字节序列或者文件对象,重点作为向服务器提供或提交资源,作为请求的请求体,与params不同放在url上不同。它也可以接受一个字符串对象。
  • json:json格式的数据,可以向服务器提交json类型的数据。
  • headers:字典,定义请求的请求头,比如可以headers字典定义user agent。
  • cookies:字典或者CookieJar。
  • auth:元组,用来支持HTTP认证功能。
  • files:字典,用来向服务器传输文件。
  • timeout:指定超时时间。
  • proxies:字典,设置代理服务器。
  • allow_redirects:开关,是否允许对URL进行重定向,默认为True。
  • stream:开关,是否对获取内容进行立即下载,默认为False,也就是立即下载。这里需要说明的,stream一般应用于流式请求,比如说下载大文件,不可能一次请求就把整个文件都下载了,不现实,这种情况下,就要设置stream=True,requests无法将连接释放回连接池,除非下载完了所有数据,或者调用了response.close。
  • verify:开关,用于SSL证书认证,默认为True。
  • cert:用于设置保存本地SSL证书路径。
  • 流式请求,指的不是请求是流,而是请求返回的数据流,返回一点取一点,而普通的请求是返回完毕你再取内容。

    分享:http://www.neeo.cc:6001/ # httpbin,提供了常用的请求方式,用于接口练习

    GET请求

    发送无参数的GET请求

    demo/demo_requests.py,代码:

    import requests
    response = requests.get('http://httpbin.org/get')
    print(response.content)  # 获取原生内容
    print(response.text)   # 获取文本内容
    

    发送有参数的GET请求

    import requests
    params = {
        'name': 'moluo',
        'age': 18
    response = requests.get("http://httpbin.org/get?sex=1", params=params)
    print(response.text)
    body = {'name': 'moluo', 'age': '22'}
    response = requests.post("http://httpbin.org/post", data=body)
    print(response.text)
    data = {'name': 'moluo', 'age': '22'}
    response = requests.post("http://httpbin.org/post", json=data)
    print(response.text)
    # 支持上传一张或多张图片
    files = {'avatar': open('1.png', 'rb')}
    response = requests.post("http://httpbin.org/post", files=files)
    print(response.text)
    
    发送请求头
    import requests
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36'
    response = requests.get("https://www.zhihu.com/explore", headers=headers)
    print(response.text)
        "http": "http://223.68.190.136:9091",
    req = requests.get('http://icanhazip.com/', proxies=proxies)
    print(req.text)
    # 1. 登录,获取cookies值
    response = requests.post(url="http://www.neeo.cc:6002/pinter/bank/api/login?userName=admin&password=1234")
    print(response.json())
    print(response.cookies)
    print(response.cookies.get_dict())
    url = 'http://httpbin.org/cookies'
    cookies = {'testCookies_1': 'Hello_Python3', 'testCookies_2': 'Hello_Requests'}
    # 在Cookie Version 0中规定空格、方括号、圆括号、等于号、逗号、双引号、斜杠、问号、@,冒号,分号等特殊符号都不能作为Cookie的内容。
    r = requests.get(url, cookies=cookies)
    print(r.json())
    
    # 现在大部分的公司在开发项目都会会考虑多终端的问题,所以往往项目整体架构都是基于前后端分离的,那么实现前后端的分离的项目,往往服务端就是一个单独基于RestFul API接口的服务端。
    import requests
    # 只有args查询字符串参数,没有请求体
    requests.get('http://httpbin.org/get')
    requests.delete('http://httpbin.org/delete')
    # 有请求体的
    requests.post('http://httpbin.org/post')
    requests.put('http://httpbin.org/put')
    requests.patch('http://httpbin.org/patch')
    

    requests/bs4爬虫练习

    import os
    import requests
    from bs4 import BeautifulSoup   # 解析爬取回来的文本,去其中找标签/标签的属性/标签的内容
    BASE_DIR = os.path.dirname(os.path.abspath(__file__))
    url = "https://www.autohome.com.cn/news/"
    response = requests.request('get', url=url, )
    # print(response.encoding)
    response.encoding = "GBK"   # 解决乱码问题
    # print(response.text)
    # 解析文本,拿到解析后的soup对象,所有解析后的数据都在soup对象中
    soup_obj = BeautifulSoup(response.text, "html.parser")   # response.text: 要解析的文本    html.parser:指定什么解析器来解析文本
    # 使用soup对象找div标签
    div_obj = soup_obj.find(name='div', attrs={"id": "auto-channel-lazyload-article"})
    # print(div_obj)
    # 根据div对象,找其内的所有的img标签
    img_list = div_obj.find_all(name="img")
    # print(img_list[0])
    for item in img_list:
        # print(item)
        # 获取标签的src属性
        img_url = "https:" + item.get("src")
        # 使用requests模块向img url发请求,获取bytes类型数据,并且保存到本地
        img_response = requests.get(img_url)
        file_name = os.path.join(BASE_DIR, 'images', img_url.rsplit("/", 1)[-1])
        with open(file_name, 'wb') as f:
            f.write(img_response.content)
        print(file_name, "download done .........")
    

    beautifulsoup4:

  • soup_obj = BeautifulSoup(response.text, "html.parser") # response.text: 要解析的文本 html.parser:指定什么解析器来解析文本
  • soup_obj.find(name=标签名, attr={”属性名":"属性值"}) 因为找到的标签可能有多个,那使用attr来过滤指定的标签。
  • soup_obj.find_all(name=标签名, attr={”属性名":"属性值"})) 找到所有以attr过滤的标签。
  • img.get('src'),表示获取指定标签的属性
  • 获取标签的文本内容,使用标签名.text
  • 自动化测试时,大家肯定都是希望自己写的代码越简洁越好,避免代码重复造成维护成本的提升,因此针对工作中常用的http网络请求模块requests,我们就需要在使用前对其进行简单的封装处理了。

    utils/requestor.py,代码:

    import requests
    from logger import LogHandle
    class Request(object):
        """http请求工具类"""
        def __init__(self):
            # 实例化session管理器,维持会话, 跨请求的时候保存参数
            self.session = requests.session()
            self.logger = LogHandle().get_logger()
        def send(self, method, url, params=None, data=None, json=None, headers=None, **kwargs):
            :param method: http请求方式
            :param url: 请求地址
            :param params: 字典或bytes,作为参数增加到url中
            :param data: data类型传参,字典、字节序列或文件对象,作为Request的内容
            :param json: json传参,作为Request的内容
            :param headers: 请求头,字典
            :param kwargs: 若还有其他的参数,使用可变参数字典形式进行传递
            :return:
            # 对异常进行捕获
                self.logger.info(f"请求方法:{method}")
                self.logger.info(f"请求地址:{url}")
                self.logger.info(f"请求头:{headers}")
                if params: self.logger.info(f"查询参数:params={params}")
                if data: self.logger.info(f"请求体:data={data}")
                if json: self.logger.info(f"请求体:json={json}")
                if kwargs: self.logger.info(f"额外参数:kwargs={kwargs}")
                response = self.session.request(method, url, params=params, data=data, json=json, headers=headers, **kwargs)
                self.logger.info(f"状态码:{response.status_code}")
                self.logger.info(f"响应头:{response.headers}")
                self.logger.info(f"响应体[纯文本]:{response.text}")
                self.logger.info(f"响应体[二进制]:{response.content}")
                self.logger.info(f"响应体[json]:{response.json()}")
                # 返回响应结果
                return response
            except Exception as e:
                # 异常处理 报错在日志中打印具体信息
                self.logger.error(f"请求失败:{e}")
        def __call__(self, method, url, params=None, data=None, json=None, headers=None, **kwargs):
            """当把一个对象,当成函数来使用,那么就指定执行当前对象的__call__"""
            return self.send(method, url, params=params, data=data, json=json, headers=headers, **kwargs)
    if __name__ == '__main__':
        """"基本使用"""
        # 实例化
        request = Request()
        """发送get请求"""
        # # response = request("GET", 'http://httpbin.org/get')
        # response = request(method="GET", url='http://httpbin.org/get')
        # # 打印响应结果
        # print(response.text)
        """发送post请求"""
        # 请求地址
        url = 'http://httpbin.org/post'
        # 请求参数
        json = {"usernam": "moluo", "password": "123456"}
        # 请求头
        headers = {"company": "luffytest"}
        response = request(method="POST", url=url, json=json, headers=headers)
        # 打印响应结果
        print(response.text)
    

    基于flask实现mockserver

    在实际项目开发中,经常出现服务端与客户端分离的情况,那么我们在做测试开发时,有可能服务端的功能还没有实现,此时我们可以自己先模拟出服务器返回结果,直接对接口基本功能进行联调测试并编写自动化测试脚本,等服务器上线之后,切换server地址,直接可以简化调试时间,缩短项目测试周期。flask是一个轻量级的python web框架,非常适用于在测试开发中构建模拟api服务器。

    pip install flask
    pip install pymysql
    pip install flask_sqlalchemy
    
    项目初始化

    api/__init__.py,代码:

    import config
    from flask import Flask
    from flask_sqlalchemy import SQLAlchemy
    # SQLAlchemy初始化
    db = SQLAlchemy()
    app = Flask(__name__)
    def init_app():
        # 加载配置
        app.config.from_object(config)
        # 加载mysql数据库配置
        db.init_app(app)
        # db创建数据表
        with app.app_context():
            db.create_all()
        return app
    

    config.py,添加配置信息,代码:

    import pathlib  # 路径操作模块,替代 os.path模块,os.path采用字符串来操作路径,pathlib采用面向对象来操作路径
    # 项目目录的主目录路径[字符串路徑]
    BASE_DIR_STR = pathlib.Path(__file__).parent.resolve().as_posix()  # 基本操作系统转换路径的分隔符 as_posix
    # 項目目录的主目录路径[路径对象]
    BASE_DIR = pathlib.Path(BASE_DIR_STR)
    # 项目名
    WEB_NAME = "路飞自动化接口测试框架-master"
    # 测试自动化项目的运行端口与IP地址
    HOST = "127.0.0.1"
    PORT = 8088
    """日志配置"""
    LOGGING = {
        "name": "luffytest",  # 日志处理器的名称,一般使用当前项目名作为名称
        "filename": (BASE_DIR / "logs/luffytest.log").as_posix(),  # 日志文件存储路径,注意,一定要在项目根目录下手动创建logs目录
        "charset": "utf-8",  # 日志内容的编码格式
        "backup_count": 31,  # 日志文件的备份数量
        "when": "d",   # 日志文件的创建间隔事件,m 表示每分钟创建1个,h表示每小时创建1个,d表示每天创建1个,m0~m6表示每周星期日~星期六创建1个,midnight表示每日凌晨
    """mock server 的服务端配置"""
    # 数据库连接
    SQLALCHEMY_DATABASE_URI: str = "mysql+pymysql://root:[email protected]:3306/pytest?charset=utf8mb4"
    # 查询时会显示原始SQL语句
    SQLALCHEMY_ECHO: bool = True
    # 调试模式
    DEBUG = True
    # 监听端口
    API_PORT = 8000
    # 监听地址
    API_HOST = "0.0.0.0"
    

    api/models.py,代码:

    from datetime import datetime
    from werkzeug.security import generate_password_hash, check_password_hash
    from . import db
    class BaseModel(db.Model):
        """公共模型"""
        __abstract__ = True  # 抽象模型
        id = db.Column(db.Integer, primary_key=True, comment="主键ID")
        name = db.Column(db.String(255), default="", comment="名称/标题")
        is_deleted = db.Column(db.Boolean, default=False, comment="逻辑删除")
        orders = db.Column(db.Integer, default=0, comment="排序")
        status = db.Column(db.Boolean, default=True, comment="状态(是否显示,是否激活)")
        created_time = db.Column(db.DateTime, default=datetime.now, comment="创建时间")
        updated_time = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now, comment="更新时间")
        def __repr__(self):
            return f"<{self.__class__.__name__}: {self.name}>"
    class User(BaseModel):
        """用户基本信息表"""
        __tablename__ = "py_user"
        name = db.Column(db.String(255), index=True, comment="用户账户")
        nickname = db.Column(db.String(255), comment="用户昵称")
        _password = db.Column(db.String(255), comment="登录密码")
        intro = db.Column(db.String(500), default="", comment="个性签名")
        avatar = db.Column(db.String(255), default="", comment="头像url地址")
        sex = db.Column(db.SmallInteger, default=0, comment="性别")  # 0表示未设置,保密, 1表示男,2表示女
        email = db.Column(db.String(32), index=True, default="", nullable=False, comment="邮箱地址")
        mobile = db.Column(db.String(32), index=True, nullable=False, comment="手机号码")
        # 存取器
        @property
        def password(self):  # user.password
            return self._password
        @password.setter
        def password(self, rawpwd):  # user.password = '123456'
            """密码加密"""
            self._password = generate_password_hash(rawpwd)
        def check_password(self, rawpwd):
            """验证密码"""
            return check_password_hash(self.password, rawpwd)
            db.session.add(user)
            db.session.commit()
            return {"msg": "注册成功!", "data": {"id":user.id, "name": user.name}}, 200
        except Exception as e:
            return {"msg": "注册失败!", "data": {}}, 400
    @app.route("/user/login", methods=["POST"])
    def login():
        :return:
        user = User.query.filter(
                User.mobile == request.json.get("username"),
                User.name == request.json.get("username"),
                User.email == request.json.get("username")
        ).first()  # 实例化模型
        if not user:
            return {"msg": "登录失败!用户不存在!", "data": {}}, 400
        if not user.check_password(request.json.get("password")):
            return {"msg": "登录失败!密码错误!", "data": {}}, 400
        return {"msg": "登录成功", "data":{"id": user.id, "name": user.name}}, 200
    # 注意,务必把模型models的内容以及 views 中的服务端接口引入当前文件,否则flask不识别。
    from api import models
    from api import views
    app = init_app()
    if __name__ == '__main__':
        app.run(host=config.API_HOST, port=config.API_PORT)
    logger = LogHandle().get_logger()
    SERVER_URl = f"http://{config.API_HOST}:{config.API_PORT}"
    @allure.epic(config.WEB_NAME)
    @allure.feature("用户模块")
    @allure.story("登录")
    class TestLogin(object):
        @allure.severity(allure.severity_level.CRITICAL)
        def test_username_by_empty(self,):
            allure.dynamic.title("用户名为空,登陆失败")
            allure.dynamic.description("测试用户名为空的登陆结果")
            # 发送请求
            request = Request()
            request("POST", f"{SERVER_URl}/user/login", json={
                "username": "",
                "password": "123456"
        def test_password_by_empty(self,):
            allure.dynamic.title("密码为空,登陆失败")
            allure.dynamic.description("测试密码为空的登陆结果")
            # 发送请求
            request = Request()
            request("POST", f"{SERVER_URl}/user/login", json={
                "username": "xiaoming",
                "password": ""
    

    基于数据驱动生成用例代码

    在实际测试开发中,我们一般使用参数化来自动生成测试用例,前面介绍过常用的有ddt与parametrize。那么在pytest中,因为本身提供了parametrize参数化,所以我们往往会在unittest中采用ddt来实现参数化,而在pytest中采用内置的parametrize即可。

    而参数化所需要的测试用例,一般我们也是可以采用json,yaml或Excel文件来存储,如果用例数量太多,还可以改成数据库保存。

    YAML(递归缩写:YAML Ain't a Markup Language,译作:YAML 不是一种标记语言)是一种可读性高,用来表达数据序列化的数据格式,使用场景与xml、json类似,2001年首次发布,在最初开发YAML语言时YAML的意思其实是:"Yet Another Markup Language"(仍是一种标记语言)。后面之所以改了名称,原因是为了强调YAML语言以数据为中心,而不是以标记语言为重点。

    1. 大小写敏感
    2. 使用缩进表示嵌套层级关系,且缩进不允许使用tab,只允许使用空格缩进(缩进的空格数不重要,只要相同层级的元素左对齐即可)
    3. '#' 表示注释
    4. '~' 表示 空(None, null),也可以使用null表示None,但是尽量不要在属性中出现null或者~
    5. yaml文件中,属性唯一不能重复,否则报错
    6. 文件扩展名为yml或yaml,如:data.yaml 或者 data.yml
    

    test.yaml,代码:

    username: 'xiaoming'
    age: 16
    对应python的数据类型
           但注意,缩进左对齐,否则报错
    # yaml中的属性声明,建议采用python变量的命名规范
    content_type: "*/*"  # 如果字符串内容中出现特殊符号,建议加上引号,否则有时候会报错
    # 布尔值 False / True
    sex: TRUE  # true,True 都可以
    # 浮点型
    score: 13.5
    point: 6.8523015e+5   # 支持科学计数法,也可以6.8523015E+5
    age: 13
    goods_id: 100_200_300  # 支持千分位
    # 日期与时间
    date: 2022-10-01
    time: 12:30:00
    datetime: 2018-02-17T15:02:31+08:00
    data1: null
    data2: ~
    null: ~  # 避免这种情况,不能使用null作为属性名!
    

    一组按次序排列的值,又称为序列(sequence) / 列表(list),类似python的列表。

    # 数组的成员可以是任意的数据类型 # 方式1:使用中括号进行数组声明,中括号可以换行也可以不换行,无需左对齐 arr1: ["A", "B", True, 1, 1.3] # 最终还是数组,属性名叫tuple,但并不代表他是元组。 arr2: [ # 方式2:使用-标记成员进行数组声明,数组成员必须缩进左对齐,否则报错,并且-与成员值必须使用空格隔开 'arr3': [1, 2, 3] arr3: # 多层数组嵌套[类似python的多维列表],这里只演示2层,还可以声明更多层嵌套 'arr4': [[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]] arr4: [ [1,2,3,4,5], [6,7,8,9,10] # 'arr5': [['1.a', '1.b'], ['2.a', '2.b']] arr5: - 1.a - 1.b - 2.a - 2.b

    键值对的集合,又称为映射(mapping)/ 哈希(hashes) / 字典(dictionary),类似python中的字典,但比字典灵活。

    # 对象的成员是键值对格式数据,属性与属性之间必须使用英文冒号(:)来映射,同时每一个之间使用英文逗号(,)隔开 # 方式1:使用花括号声明,花括号可以换行,无需左对齐 dict: {name: xiaoming, age: 17} map: { name: xiaohong, age: 16, sex: false # 方式2:使用缩进键值对成员进行对象声明,对象成员必须缩进左对齐,不对齐则报错 data: name: xiaoming age: 16 # 多层嵌套对象[类似python的多维字典] # 'people': {'name': 'xiaoming', 'age': 26, 'love': ['code', 'watching TV', 'shopping'], 'son': {'name': 'xiaohui', 'age': 3}} people: name: xiaoming age: 26 love: - code - watching TV - shopping name: xiaohui age: 3 goods_list: name: 立白洗衣粉 price: 39.90 num: 3 name: 黑猫神蚊香液 price: 56.50 num: 6

    python操作YAML

    安装yaml模块

    pip install pyyaml
    
    import yaml
    """读取yaml文件的数据"""
    # with open("./data.yaml", "r",encoding="utf-8") as f:
    #     content = f.read()
    #     data = yaml.load(content, Loader=yaml.FullLoader)
    #     print(data)
    #     print(data["name"])
    """把数据写入yaml文件"""
    from datetime import datetime
    with open("./data2.yaml", "w", encoding="utf-8") as f:
        # yaml.dump(data, f, Dumper=yaml.SafeDumper) # 没有多字节内容的情况下
        data = {
            "name": "xiaoming",
            "age": 17,
            "datetime": datetime.now(),
            "point": 1.245464E10,
            "goods_list": [
                {"name": "xiaoming","age": 17, "sex": True},
                {"name": "xiaoming","age": 17, "sex": True},
                {"name": "xiaoming","age": 17, "sex": True},
                {"name": "xiaoming","age": 17, "sex": True},
            "author_list": ["小明", "小白", "小红"],
            "user_info": {"username":"小明", "password": "123456"}
        yaml.dump(data, f, Dumper=yaml.SafeDumper, allow_unicode=True)  # 有多字节内容的情况下,中文就是多字节内容
            if not cls.__instance:
                print("创建Yaml的单例")
                cls.__instance = super(Yaml, cls).__new__(cls, *args, **kwargs)
            return cls.__instance
        def __init__(self):
            self.logger = LogHandle().get_logger()
        def read(self, path):
            """读取yaml文件"""
            with open(path, encoding="utf-8") as f:
                result = f.read()
                if result:
                    result = yaml.load(result, Loader=yaml.FullLoader)
                return result
        def write(self, path, data):
            """写入yaml文件"""
                with open(path, "w", encoding="utf-8") as f:
                    yaml.dump(data, f, Dumper=yaml.SafeDumper, allow_unicode=True)
                return True
            except Exception as e:
                self.logger(f"写入数据到yaml文件失败:{e}")
                return False
    if __name__ == '__main__':
        ya = Yaml()
        data = ya.read("../demo/yaml_demo/data.yaml")
        print(data, type(data))
    logger = LogHandle().get_logger()
    SERVER_URl = f"http://{config.API_HOST}:{config.API_PORT}"
    yaml = Yaml()
    @allure.epic(config.WEB_NAME)
    @allure.feature("用户模块")
    @allure.story("登录")
    class TestLogin(object):
        # @allure.severity(allure.severity_level.CRITICAL)
        # def test_username_by_empty(self,):
        #     allure.dynamic.title("用户名为空,登陆失败")
        #     allure.dynamic.description("测试用户名为空的登陆结果")
        #     # 发送请求
        #     request = Request()
        #     request("POST", f"{SERVER_URl}/user/login", json={
        #         "username": "",
        #         "password": "123456"
        #     })
        # def test_password_by_empty(self,):
        #     allure.dynamic.title("密码为空,登陆失败")
        #     allure.dynamic.description("测试密码为空的登陆结果")
        #     # 发送请求
        #     request = Request()
        #     request("POST", f"{SERVER_URl}/user/login", json={
        #         "username": "xiaoming",
        #         "password": ""
        #     })
        @pytest.mark.parametrize("kwargs", yaml.read(config.BASE_DIR / "data/user_login.yaml"))
        def test_login(self, kwargs):
            request = Request()
            allure.dynamic.title(kwargs.get('name'))
            request.logger.info(f"开始请求测试接口:{kwargs.get('name')}")
            data = kwargs.get('request')
            response = request(data.get("method"), f'{SERVER_URl}{data.get("url")}', json=data.get("json"))
            assertor(kwargs.get("assert"), response)
    

    Excel

    在测试开发中,如果测试用例数量太多,使用yaml也存在很大的维护成本,此时可以考虑使用Excel或者数据库保存更多的测试用例,python中操作Exeel文件的模块有很多,常用的有:xlrd+xlwt,pyexcel+openpyxl等等。

    pip install xlrd
    pip install xlwt
    

    封装Excel工具类

    utils/excel.py,代码:

    import xlrd, json
    class Excel(object):
        """Excel文件操作工具类"""
        def __init__(self, filename):
            self.workbook = xlrd.open_workbook(filename, formatting_info=True)
        def get_sheet_names(self):
            获取当前excel文件所有的工作表的表名
            :return:
            return self.workbook.sheet_names()
        def __get_sheet(self, sheet_index_or_name):
            根据sheet的索引或名称,获取sheet对象
            :param sheet_index_or_name: sheet的索引或名称
            :return:sheet对象
            if isinstance(sheet_index_or_name, int):
                if len(self.workbook.sheet_names()) > sheet_index_or_name:
                    return self.workbook.sheet_by_index(sheet_index_or_name)
                else:
                    raise Exception("Invalid Sheet Index!")
            elif isinstance(sheet_index_or_name, str):
                if sheet_index_or_name in self.workbook.sheet_names():
                    return self.workbook.sheet_by_name(sheet_index_or_name)
                else:
                    raise Exception("Invalid Sheet Name!")
        def get_rows_num(self,sheet_index_or_name):
            获取指定工作表的数据总行数
            :param sheet_index_or_name: 工作表名或索引
            :return:
            return self.__get_sheet(sheet_index_or_name).nrows
        def get_cols_num(self,sheet_index_or_name):
            获取指定工作表的数据总列数
            :param sheet_index_or_name: 工作表名或索引
            :return:
            return self.__get_sheet(sheet_index_or_name).ncols
        def get_cell_value(self, sheet_index_or_name, row_index, col_index):
            获取指定工作表的指定位置的数据值
            :param sheet_index_or_name: 工作表名或索引
            :param row_index: 行下标,从0开始
            :param col_index: 列下标,从0开始
            :return:
            sheet = self.__get_sheet(sheet_index_or_name)
            if sheet.nrows and sheet.ncols:
                return sheet.cell_value(row_index, col_index)
            else:
                raise Exception("Index out of range!")
        def get_data(self, sheet_index_or_name, fields, first_line_is_header=True):
            获取工作表的所有数据
            :param sheet_index_or_name: 工作表名或索引
            :param fields: 返回数据的字段名
            :param first_line_is_header: 工作表是否是否表头,也就是非数据
            :return:
            rows = self.get_rows_num(sheet_index_or_name)
            cols = self.get_cols_num(sheet_index_or_name)
            data = []
            for row in range(int(first_line_is_header), rows):
                row_data = {}
                for col in range(cols):
                    cell_data = self.get_cell_value(sheet_index_or_name, row, col)
                    if type(cell_data) is str and ("{" in cell_data and "}" in cell_data or "[" in cell_data and "]" in cell_data):
                        """判断如果表格中填写的数据是json格式键值对,则采用json模块转换成字典"""
                        cell_data = json.loads(cell_data)
                    row_data[fields[col]] = cell_data
                data.append(row_data)
            return data
    if __name__ == '__main__':
        xls = Excel("../data/case_user.xls")
        fields = [
            "case_id",
            "module_name",
            "case_name",
            "method",
            "url",
            "headers",
            "params_desc",
            "params",
            "assert_result",
            "real_result",
            "remark",
        print(xls.get_data(0, fields))
        {'case_id': 1.0, 'module_name': '用户模块', 'case_name': '用户登录-测试用户名为空的情况', 'method': 'post', 'url': 'http://127.0.0.1:8000/user/login', 'headers': '', 'params_desc': 'username: 用户名\npassword: 密码', 'params': {'username': '', 'password': '123456'}, 'assert_result': 'code==400', 'real_result': '', 'remark': ''}, 
        {'case_id': 2.0, 'module_name': '用户模块', 'case_name': '用户登录-测试密码为空的情况', 'method': 'post', 'url': 'http://127.0.0.1:8000/user/login', 'headers': '', 'params_desc': 'username: 用户名\npassword: 密码', 'params': {'username': 'xiaoming', 'password': ''}, 'assert_result': 'code==400', 'real_result': '', 'remark': ''}, 
        {'case_id': 3.0, 'module_name': '用户模块', 'case_name': '用户登录-测试账号密码正确的情况', 'method': 'post', 'url': 'http://127.0.0.1:8000/user/login', 'headers': '', 'params_desc': 'username: 用户名\npassword: 密码', 'params': {'username': 'xiaoming', 'password': '123456'}, 'assert_result': 'code==200', 'real_result': '', 'remark': ''}, 
        {'case_id': 4.0, 'module_name': '用户模块', 'case_name': '用户登录-测试使用手机号码登录', 'method': 'post', 'url': 'http://127.0.0.1:8000/user/login', 'headers': '', 'params_desc': 'username: 手机号\npassword: 密码', 'params': {'username': '13312345678', 'password': '123456'}, 'assert_result': 'code==200', 'real_result': '', 'remark': ''}]
    
    import pathlib  # 路径操作模块,替代 os.path模块,os.path采用字符串来操作路径,pathlib采用面向对象来操作路径
    # 项目目录的主目录路径[字符串路徑]
    BASE_DIR_STR = pathlib.Path(__file__).parent.resolve().as_posix()  # 基本操作系统转换路径的分隔符 as_posix
    # 項目目录的主目录路径[路径对象]
    BASE_DIR = pathlib.Path(BASE_DIR_STR)
    # 项目名
    WEB_NAME = "路飞自动化接口测试框架-master"
    # 测试自动化项目的运行端口与IP地址
    HOST = "127.0.0.1"
    PORT = 8088
    """日志配置"""
    LOGGING = {
        "name": "luffytest",  # 日志处理器的名称,一般使用当前项目名作为名称
        "filename": (BASE_DIR / "logs/luffytest.log").as_posix(),  # 日志文件存储路径,注意,一定要在项目根目录下手动创建logs目录
        "charset": "utf-8",  # 日志内容的编码格式
        "backup_count": 31,  # 日志文件的备份数量
        "when": "d",   # 日志文件的创建间隔事件,m 表示每分钟创建1个,h表示每小时创建1个,d表示每天创建1个,m0~m6表示每周星期日~星期六创建1个,midnight表示每日凌晨
    # excel测试用例字段格式
    FIELD_LIST = [
        "case_id",        # 用例编号
        "module_name",    # 模块名称
        "case_name",      # 用例名称
        "method",         # 请求方式
        "url",            # 接口地址
        "headers",        # 请求头
        "params_desc",    # 参数说明
        "params",         # 请求参数
        "assert_result",  # 预期结果
        "real_result",    # 实际结果
        "remark",         # 备注
    """mock server 的服务端配置"""
    # 数据库连接
    # SQLALCHEMY_DATABASE_URI: str = "数据库类型://账户:密码@IP地址:端口/数据库名称?charset=utf8mb4"
    SQLALCHEMY_DATABASE_URI: str = "mysql+pymysql://root:[email protected]:3306/pytest?charset=utf8mb4"
    # 查询时会显示原始SQL语句
    SQLALCHEMY_ECHO: bool = True
    # 调试模式
    DEBUG = True
    # 监听端口
    API_PORT = 8000
    # 监听地址
    API_HOST = "127.0.0.1"
    logger = LogHandle().get_logger()
    SERVER_URl = f"http://{config.API_HOST}:{config.API_PORT}"
    yaml = Yaml()
    @allure.epic(config.WEB_NAME)
    @allure.feature("用户模块")
    @allure.story("登录")
    class TestLogin(object):
        # @allure.severity(allure.severity_level.CRITICAL)
        # def test_username_by_empty(self,):
        #     allure.dynamic.title("用户名为空,登陆失败")
        #     allure.dynamic.description("测试用户名为空的登陆结果")
        #     # 发送请求
        #     request = Request()
        #     request("POST", f"{SERVER_URl}/user/login", json={
        #         "username": "",
        #         "password": "123456"
        #     })
        # def test_password_by_empty(self,):
        #     allure.dynamic.title("密码为空,登陆失败")
        #     allure.dynamic.description("测试密码为空的登陆结果")
        #     # 发送请求
        #     request = Request()
        #     request("POST", f"{SERVER_URl}/user/login", json={
        #         "username": "xiaoming",
        #         "password": ""
        #     })
        # @pytest.mark.parametrize("kwargs", yaml.read(config.BASE_DIR / "data/user_login.yaml"))
        # def test_login(self, kwargs):
        #     """数据驱动自动化测试-基于yaml生成"""
        #     request = Request()
        #     allure.dynamic.title(kwargs.get('name'))
        #     request.logger.info(f"开始请求测试接口:{kwargs.get('name')}")
        #     data = kwargs.get('request')
        #     response = request(data.get("method"), f'{data.get("url")}', json=data.get("json"))
        #     assertor(kwargs.get("assert"), response)
        @pytest.mark.parametrize("kwargs", Excel(config.BASE_DIR / "data/case_user.xls").get_data(0, config.FIELD_LIST))
        def test_login(self, kwargs):
            """数据驱动自动化测试-基于excel生成"""
            request = Request()
            allure.dynamic.title(kwargs.get('case_name'))
            request.logger.info(f"开始请求测试接口:{kwargs.get('case_name')}")
            if kwargs.get("method").lower() in ["get", "delete"]:
                """发送get或delete"""
                response = request(kwargs.get("method"), f'{kwargs.get("url")}', params=kwargs.get("params"))
            else:
                """发送post,put,patch"""
                response = request(kwargs.get("method"), f'{kwargs.get("url")}', json=kwargs.get("params"))
            assertor(kwargs.get("assert_result"), response)
    

    基于jwt实现登陆正确,并在pytest中基于conftest脚手架使用生成器保持登陆状态

    在用户注册/登陆以后,往往项目会返回登陆状态(jwt,session, cookie)提供给客户端,所以上面我们所实现的mockserver实际上是有问题的。因此接下来我们继续来模拟存在jwt认证鉴权的服务端,并在测试框架中基于conftest.py来实现认证测试的这个流程。

    安装jwt

    pip install flask-jwt-extended
    

    config.py,代码:

    # 秘钥,不管是使用session还是jwt认证,都需要对认证的信息鉴权加密
    SECRET_KEY = "ac361a52518d99f4525c1cfe5ba635572190aa6ac52bc8f27ae1b07529feafd0"
    

    api/__init__.py,代码:

    import config
    from flask import Flask
    from flask_sqlalchemy import SQLAlchemy
    from flask_jwt_extended import JWTManager
    # SQLAlchemy初始化
    db = SQLAlchemy()
    app = Flask(__name__)
    jwt = JWTManager()
    def init_app():
        """服务端初始化"""
        # 加载配置
        app.config.from_object(config)
        # 加载mysql数据库配置
        db.init_app(app)
        # jwt初始化
        jwt.init_app(app)
        # 自动创建数据表
        with app.app_context():
            db.create_all()
        return app
    

    api/views.py,代码:

    from flask import request
    from sqlalchemy import or_
    from flask_jwt_extended import jwt_required, create_access_token
    from . import app
    from .models import User,db
    @app.route("/user/register", methods=["POST"])
    def register():
        用户信息注册
        :return:
            data = request.json
            # 创建用户数据
            user = User(**data)
            db.session.add(user)
            db.session.commit()
            return {"msg": "注册成功!", "data": {"id":user.id, "name": user.name}}, 200
        except Exception as e:
            return {"msg": "注册失败!", "data": {}}, 400
    @app.route("/user/login", methods=["POST"])
    def login():
        :return:
        user = User.query.filter(
                User.mobile == request.json.get("username"),
                User.name == request.json.get("username"),
                User.email == request.json.get("username")
        ).first()  # 实例化模型
        if not user:
            return {"msg": "登录失败!用户不存在!", "data": {}}, 400
        if not user.check_password(request.json.get("password")):
            return {"msg": "登录失败!密码错误!", "data": {}}, 400
        # 生成token,并返回给客户端
        access_token = create_access_token(identity={"id": user.id, "username": user.name})
        return {"msg": "登录成功", "data": {"token": access_token}}, 200
    @app.route("/user", methods=["GET"])
    @jwt_required()   # 当前这个装饰器的作用就是 校验用户是否登录
    def user_center():
        return {"errmsg": "访问成功"}
        request = Request()
        request.logger.info("获取token")
        data = yaml.read(config.BASE_DIR / "data/test_user.yaml")
        response = request(data.get("method"), data.get("url"), json=data.get("json"))
        token = response.json().get("data", {}).get("token")
        yield token
        # 生成器函数中的暂停关键字,作用是当代码运行到yield时,把yield右边的数据作为返回值提供给调用处,把代码执行权交出去。
        request.logger.info("移除token")
    

    data/test_user.yaml,填写一个保存正确用户信息的用例,方便在conftest中发送正确的账户信息获取token,代码:

    method: post
    url: http://127.0.0.1:8000/user/login
    json:
      "username": "xiaoming"
      "password": "123456"
    

    新建一个测试用例,tests/test_user.py,代码:

    import allure
    import pytest
    import config
    from utils.requestor import Request
    from utils.yamler import Yaml
    from utils.assertor import assertor
    yaml = Yaml()
    @allure.epic(config.WEB_NAME)
    @allure.feature("用户模块")
    @allure.story("用户中心")
    class TestUser(object):
        @pytest.mark.usefixtures("jwt_token")
        @pytest.mark.parametrize("kwargs", yaml.read(config.BASE_DIR / "data/user_info.yaml"))
        def test_user(self, jwt_token, kwargs):
            allure.dynamic.title(kwargs.get('name'))
            request = Request()
            request.logger.info(f"开始请求测试接口:{kwargs.get('name')}")
            data = kwargs.get('request')
            data['headers']["Authorization"] = data['headers']["Authorization"].format(token=jwt_token)
            response = request(data.get("method"), data.get("url"), headers=data.get("headers"))
            assertor(kwargs.get("assert"), response)
    

    参考:https://docs.python.org/zh-cn/3/library/subprocess.html

    参考:https://www.cnblogs.com/Neeo/articles/11934072.html

    处理cookie

  • 在每个用例发请求的时候,查看响应结果是否有cookies返回
  • 如果有cookie返回,就把保存起来
  • 保存到本地指定目录,以域名命名,当有接口需要cookies的时候,去指定目录里根据域名匹配查找cookies,然后携带。(相似Postman)
  • 将cookies保存到当前的用例对象中,可以为用例对象新建一个临时字段,来保存cookies,谁要用,就来找就完了。
  • 问题:如何获取域名?

    从url上切
    
    # import requests
    # # response = requests.request(method='post', url='http://www.neeo.cc:6002/pinter/bank/api/login2', data={"userName":"admin", "password":1234})
    # # data = response.json()
    # response = requests.request(method='post', url='http://www.neeo.cc:6002/pinter/bank/api/login2', data={"userName":"admin", "password":1234})
    # data = response.json()
    # print(data)
    # response = requests.request(method='get', url='http://www.neeo.cc:6002/pinter/bank/api/query2', params={"userName": "admin"},
    #                             headers={"testfan-token": data['data']}
    #                             )
    # print(response.json())
    data = {
        'code': '0',
        'message': 'success',
        'data': '95e947dd5a684105b97cf74d3a4514a8',
        "info": {
            "address": "北京",
            "phone": "1212313123"
        "item": [
            {"user": "zhangkai"},
            {"user": "wuaihua"},
        "userName": "zhangkai",
        "password": 1234,
    # print(data['info']['phone'])
    # a = 'info.address'
    a = 'item.[1].user'
    # a = '$..user'
    from jsonpath_rw import parse
    # js_exe = parse(a)
    # match = js_exe.find(data)
    # print([i.value for i in match])
    import re
    import json
    data2 = {
        "userName": "${neeo_002>params>userName}$",
        "password": "${neeo_003>data>password}$",
        "user": "${neeo_003>data>item.[0].user}$"
    pattern = re.compile('\${(.*?)}\$')
    match_list = pattern.findall(json.dumps(data2))
    print(match_list)
    for i in match_list:
        case, params, json_path = i.split(">")
        print(case, params, json_path)
        match = parse(json_path).find(data)
        temp = [v.value for v in match][0]
        # re.match(json_path, json.dumps(data2), temp)
        data2= re.sub(pattern, temp, json.dumps(data2), 1)
        print(data2)
        # break
    JsonPath
    pip install jsonpath-rw
    正则匹配: ${neeo_001>data>data.info.address.phone}$   --> neeo_001>data>data.info.address.phone  --> abc
    正则替换:${neeo_001>data>data.info.address.phone}$ --> abc
    ${neeo_001>data>data.info.address.phone}$
    ${neeo_001>headers>data}$
    ${neeo_001>params>data}$
    neeo_001 data data.info.address.phone
        - neeo_001:依赖数据来自于哪个用例
        - data: 来自于哪个用例中的什么参数:
            - response.json
            - headers
            - cookies
            - params
        - data.info.address.phone: 数据所在的路径
    neeo_001 data data
    case_num 
    

    代码执行pytest.main或者终端执行pytest均生成不了json数据

     # 启动pytest框架
        # pytest.main()   # 此方法不可靠,执行时中间夹杂别的py文件导致allure生成不了json数据
        os.system('pytest -s -v --alluredir=./allure_results --clean-alluredir')
        # 生成报告html文件
        os.system('allure generate ./allure_results -o ./allure_reports')
        # 基于http协议打开HTML测试报告
        # os.system(f'allure open ./allure_reports -h {config.HOST} -p {config.PORT}')
    

    持续集成-jenkins

    docker篇:

  • 基础篇 | 各平台搭建docker环境 | 快速上手(容器、镜像、网络、数据卷) | docker-compose
  • 搭建Selenium Grid hub分布式UI测试框架 | docker安装MySQL | docker安装redis | docker配置httpbin项目 | docker配置Tomcat并添加Java web项目 | docker部署Django项目 | 容器迁移 | 搭建FTP服务器 |
  • 常见报错 | 解决拉取镜像慢的问题
  • Jenkins篇:

  • 初始Jenkins | Docker关于Jenkins镜像的那些事儿 | 各平台配置Jenkins环境 | Jenkins配置Python环境 | 插件管理
  • Jenkins环境配置allure-commandline | jenkins邮件配置 | Jenkins构建接口自动化脚本 | Jenkins构建Djando项目
  • 常用配置 | 凭据管理 | 用户管理
  • 常见报错处理 | Jenkins容器迁移
  • 什么是持续集成

    持续集成是一种软件开发实践经验,采用持续集成时,开发人员会定期将他们的代码变更合并到一个中央存储库中,之后系统会自动运行构建和测试操作。持续集成的主要目标是更快发现并解决错误,提高软件质量,并缩短验证和发布新软件更新所需的时间。

    持续交付是一种软件开发实践。通过持续交付,系统可以自动构建和测试代码更改,并为将其发布到生产环境做好准备。持续交付可以在构建阶段后将所有代码变更都部署到测试环境和/或生产环境中,从而实现对持续集成的扩展。当持续交付得以正确实施时,开发人员将始终能够获得一个已通过标准化测试流程的部署就绪型构建工件。

  • https://aws.amazon.com/cn/devops/what-is-devops/
  • https://www.jianshu.com/p/5643b1cf9e3f
  • https://www.cnblogs.com/Neeo/p/10428714.html
  • 关于jenkins

    Jenkins是一个开源项目,是基于Java开发的集成工具。Jenkins是一款开源CI&CD软件,用于自动化各种任务,包括构建、测试和部署软件.
    Jenkins支持各种运行方式,可通过系统包,Docker或者通过一个独立的Java程序。

    文档:https://www.jenkins.io/zh/doc/

    jenkins安装

    前提:有java 环境

    支持各种的平台:

  • Windows,有msi
  • Tomcat环境,可以部署到Windows、Mac OS、linux
  • docker,Windows、Mac OS、linux
  • 最低推荐配置:

  • 256MB可用内存
  • 1GB可用磁盘空间(作为一个Docker容器运行jenkins的话推荐10GB)
  • 为小团队推荐的硬件配置:

  • 1GB+可用内存
  • 50 GB+ 可用磁盘空间
  • 软件配置:

  • Java 8—无论是Java运行时环境(JRE)还是Java开发工具包(JDK)都可以。
  • jenkins for docker

    本次安装环境:阿里云服务器(centos7.4) + docker 19.03.8

    常用的镜像有两个:

    docker pull jenkins:latest
    docker pull jenkinsci/blueocean:latest
    
  • 下载jenkins镜像
  • [root@r ~]# docker pull jenkinsci/blueocean:latest
    [root@r ~]# docker images |grep jenkins
    jenkinsci/blueocean   latest              789f2766377f        15 hours ago        567MB
    
  • 启动docker容器
  • docker run \
      -u root \
      --name myjenkins \
      --restart=always \
      -p 6010:8080 \
      -p 50000:50000 \
      --env JAVA_OPTS="-Xmx512m" \
      -e JAVA_OPTS=-Duser.timezone=Asia/Shanghai \
      -v /etc/localtime:/etc/localtime \
      -v /tmp/jenkins_home:/var/jenkins_home \
      -v /var/run/docker.sock:/var/run/docker.sock \
      jenkinsci/blueocean:latest
    
  • 启动后,会默认的创建一个admin用户(只有基于jenkinsci/blueocean:latest镜像的jenkins容器会创建一个admin用户),并且生成一个初始密码:
  • # 查看密码,仅除此使用
    [root@r ~]# docker logs -t -f --tail 40 myjenkins
    # 查看密码,推荐
    [root@r ~]# docker exec -it -u root myjenkins bash
    bash-5.1# cat /var/jenkins_home/secrets/initialAdminPassword 
    e2be0a4bd7db42dca7c47c60fcdc636a
    
  • 使用初始密码解锁jenkins。
  • 新手入门之,安装推荐的插件:
  • 管理jenkins ---> manges plugins ---> 可选插件,搜索要安装的插件,可选择,安装并且重启jenkins

    由于下载地址是插件官网,可能会导致下载失败,然后安装失败.....

    如果安装失败,就采用第二种方式。

  • 手动下载插件,参考网址:https://updates.jenkins.io/download/plugins/,这里以allure插件为例:
  • 然后手动将下载到本地的hpi插件,上传到jenkins。

    管理jenkins ---> manges plugins ---> 高级选项,下拉选择上传插件。点击本地文件上传

    完事之后,重启jenkins,插件生效。

    管理jenkins ---> manges plugins ---> 已安装,搜索要卸载的插件,并且勾选然后点击卸载。

    解决:插件下载慢的问题

    修改/var/jenkins_home/hudson.model.UpdateCenter.xml 文件,换国内源:

    bash-4.4# cat /var/jenkins_home/hudson.model.UpdateCenter.xml 
    <?xml version='1.1' encoding='UTF-8'?>
    <sites>
        <id>default</id>
        <url>https://updates.jenkins.io/update-center.json</url>
      </site>
    </sites>
    bash-4.4# vi /var/jenkins_home/hudson.model.UpdateCenter.xml 
    bash-4.4# cat /var/jenkins_home/hudson.model.UpdateCenter.xml 
    <?xml version='1.1' encoding='UTF-8'?>
    <sites>
        <id>default</id>
        <url>https://mirrors.tuna.tsinghua.edu.cn/jenkins/updates/update-center.json</url>
      </site>
    

    常用的国内源地址:函数的返回值

    https://mirrors.tuna.tsinghua.edu.cn/jenkins/updates/update-center.json
    http://mirror.esuni.jp/jenkins/updates/update-center.json
    http://mirror.xmission.com/jenkins/updates/update-.json
    

    常用的操作:

    必要的配置

    管理jenkins ---> 全局安全配置,勾选允许用户注册,完事点击保存。

  • manage jenkins ---> mange user ,用户列表,选择用户id。
  • 选择设置,下拉重新输入新的密码,然后下拉点击确认按钮。
  • 管理jenkins ---> 管理用户

    点击新建用户

    创建成功后的用户列表:

    这里只能删除普通的用户。

    管理jenkins ---> 管理用户,点击红色按钮进行删除。

    确认删除:

    由于jenkins要和别的软件或者平台打交道,那么就要拿着先关凭据去做认证。

    jenkins主页 ---> 凭据 ---> 全局凭据

    此时进入到了全局的凭据列表,列出了所有的凭据。

    如何添加凭据呢?

    点击左侧的添加凭据按钮。

    创建成功,会在凭据列表展示出来,可以点击右侧按钮编辑该凭据。

    在凭据列表中,点击指定凭据后的小三角或者右侧的更新按钮,来修改凭据。

    来修改相关内容。

    凭据列表,选择指定凭据后的小三角,选择删除选项。

    确认删除。

    在上述的配置GitHub账号密码的凭据中,有的时候会遇到如下问题:

    如何解决:

    可以使用ssh形式来解决:

    配置github公钥私钥凭据

    生成公钥私钥

    github对SSH密钥做了升级,原来的SHA-1,rsa等一些已经不支持了,由于我使用的是rsa,可能和大部分用户一样,所以今天在push代码时候遇到了这个问题,记录以下。

    在本机使用git来生成公钥私钥:

    ssh-keygen -t ed25519 -C "your-email"
    # 示例,一路回车
    $ ssh-keygen -t ed25519 -C "your-email"
    Generating public/private rsa key pair.
    Enter file in which to save the key (/c/Users/Anthony/.ssh/id_rsa):
    Enter passphrase (empty for no passphrase):
    Enter same passphrase again:
    Your identification has been saved in /c/Users/Anthony/.ssh/id_rsa.
    Your public key has been saved in /c/Users/Anthony/.ssh/id_rsa.pub.
    The key fingerprint is:
    SHA256:IpMirm5XMasMF74a2ti1RSXZyiu35hORaYTYfkoWIz0 [email protected]
    The key's randomart image is:
    +---[RSA 2048]----+
    |   + .           |
    |  o E .o         |
    |   o =ooo        |
    |   .+=*+         |
    |. oo=+O.S        |
    |.o +.*.o         |
    | o+ * +.         |
    |o=.* =o.         |
    |*o= .oo.         |
    +----[SHA256]-----+
    

    错误参考:https://www.cnblogs.com/my_captain/p/11020381.html

    配置python环境

    不同的镜像依赖的基础镜像不同,导致容器内容的包管理工具也不同,如何查看以来的基础镜像:

    [root@r docker_data]# docker exec -it -u root myjenkins bash
    bash-4.4# cat /etc/issue 
    Welcome to Alpine Linux 3.9
    Kernel \r on an \m (\l)
    apk upgrade            # 升级所有软件
    apk upgrade openssh    # 升级指定软件
    apk upgrade openssh  vim  bash nginx # 升级多个软件
    apk add --upgrade busybox  # 指定升级部分软件包
    # 搜索包
    apk search                 # 查找所有可用软件包
    apk search -v              # 查找所用可用软件包及其描述内容
    apk search -v ‘包名’        # 通过软件包名称查找软件包
    apk search -v -d ‘docker’  # 通过描述文件查找特定的软件包
    # 安装包
    apk add 包名
    apk add python3
    apk add openssh                       # 安装一个软件
    apk add openssh  vim  bash nginx      # 安装多个软件
    apk add --no-cache mysql-client       # 不使用本地镜像源缓存,相当于先执行update,再执行add
    apk add python3=3.6.9-r2
    apk add python2=2.7.18-r0
    # 查看包信息
    apk info           # 列出所有已安装的软件包
    apk info -a zlib   # 显示完整的软件包信息
    apk info --who-owns /usr/sbin/nginx # 显示指定文件属于的包
    # 删除包
    apk del 包名
    apk del openssh      # 删除一个软件
    apk del nginx mysql  # 删除多个软件
    # 配置源的方法
    localhost:/etc/apk# vim /etc/apk/repositories 
    //阿里云源
    http://mirrors.aliyun.com/alpine/v3.15/main
    http://mirrors.aliyun.com/alpine/v3.15/community
    #  Alpine Linux服务管理
    rc-update    # 主要用于不同运行级增加或者删除服务。
    rc-status    # 主要用于运行级的状态管理。
    rc-service   # 主用于管理服务的状态
    openrc       # 主要用于管理不同的运行级。
    # 重启网络服务: rc-service networking restart
    # 列出所有服务: rc-status -a
    

    更多参考:https://zhuanlan.zhihu.com/p/466105234

    安装python3.6

  • 更新源列表和安装依赖
  • bash-4.4# apk update -y
    bash-4.4# apk add gcc
    bash-4.4# apk add build-base
    bash-4.4# apk add zlib-dev
    <body leftmargin="8" marginwidth="0" topmargin="8" marginheight="4"
          offset="0">
    <table width="95%" cellpadding="0" cellspacing="0"
           style="font-size: 11pt; font-family: Tahoma, Arial, Helvetica, sans-serif">
                    <b>来自Jenkins的邮件通知</b>
                <b style="color:#0B610B;">构建信息:</b>
                <hr size="2" width="100%" align="center"/>
                    <li>项目名称&nbsp;:&nbsp;${PROJECT_NAME}</li>
                    <li>触发原因&nbsp;:${CAUSE}</li>
                    <li>构建日志&nbsp;:&nbsp;<a href="${BUILD_URL}console">${BUILD_URL}console</a></li>
                    <li>单元测试报告&nbsp;:<a href="${BUILD_URL}allure/">${BUILD_URL}allureReport/</a></li>
                    <li>工作目录&nbsp;:&nbsp;<a href="${PROJECT_URL}">${PROJECT_URL}</a></li>
                    <li>测试报告下载&nbsp;:&nbsp;<a href="${PROJECT_URL}">${PROJECT_URL}lastSuccessfulBuild/artifact/allure-report.zip</a></li>
            <td><b style="color:#0B610B;">构建日志:</b>
                <hr size="2" width="100%" align="center"/>
            <td><textarea cols="80" rows="30" readonly="readonly"
                          style="font-family: Courier New;width: 500px;max-width: 1000px;">${BUILD_LOG}</textarea>
    </table>
    </body>
    </html>
    

    上面模板的含义参考可用变量列表:

  • 首先要保证Email Extension Plugin已下载。
  • 确认管理员邮件地址
  • 填写smtp等信息,点击高级
  • 按照下图配置
  • 注意,上述6~9都是针对于该项目的特殊配置,如果没有特殊的配置,就是用系统配置中的相关参数。

    配置java jdk/git/Allure Commandline

    配置java jdk

    找到容器内容的JAVA_HOME

    bash-4.4# echo $PATH
    /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/lib/jvm/java-1.8-openjdk/jre/bin:/usr/lib/jvm/java-1.8-openjdk/bin
    bash-4.4#