Web自动化测试小结
一、待测试功能模块
本次自动化测试所涉及的功能模块为:文章发布与审核,具体流程为:
- 用户通过前端页面新建并发表文章
- url:http://ttmp.research.itcast.cn/#/login
- 账号:13812345678
- 密码:246811
- 管理员通过后台管理系统对文章进行审核
- url:http://ttmis.research.itcast.cn/#/
- 账号:testid
- 密码:testpwd123
二、自动化测试流程
- 需求分析
- 挑选适合做自动化测试的功能模块
- 设计测试用例
- 搭建自动化测试环境【可选】 -- python、pycharm、selenium等
- 设计自动化测试项目的框架【可选】 -- base、page、scripts、datas、tool等
- 编写代码
- 执行测试用例
- 生成测试报告并分析结果
- 持续集成
【说明】
由于自动化测试是在功能测试完成之后实施的,因此将上述的测试流程进行简化:将“需求分析、挑选适合做自动化测试的功能模块、设计测试用例” 合并为“抽取功能测试用例转化成在自动化测试用例”
三、编写自动化测试用例的原则
(1)自动化测试用例的抽取原则
- 自动化测试用例一般只实现核心业务流程或者重复执行效率较高的功能;
- 自动化测试用例的选择一般以“正向”逻辑的验证为主;
- 并不是所有的手工用例都可以使用自动化测试来执行,如页面的布局是否合理
(2)测试脚本的编写原则
- 尽量减少多个用例脚本之间的依赖;
- 自动化测试用例执行完成之后,一般需要回归原点,如在测试过程中向数据库中添加的记录,在执行完测试用例之后一般需要删除。
四、编写测试用例
(1)前端页面
ID | 模块 | 优先级 | 测试标题 | 预置条件 | 步骤描述 | 测试数据 | 预期结果 | 测试结果 |
mt001 | 登录 | P0 | 登录成功 | 1、打开登录页面 | 1、输入用户名 2、输入密码 3、点击登录按钮 |
1、用户名:13812345678 2、密码:246811 |
1、用户登录成功,页面顶部显示用户名信息 | |
mt002 | 内容管理 | P0 | 发布文章 | 1、用户成功登录 2、进入内容管理页面 |
1、点击“发布文章”菜单 |
1、标题 |
1、提示:文章新增成功 |
(2)后台管理系统
五、项目搭建
(1)初始化项目
- 项目名称
(2)创建目录结构
- 目录结构包括:base、page、scripts、data、log、report、tool、image
【关于项目目录结构的说明】
- base:所有page页面的基类 -- 用于提取page页面的公共方法,如元素查找、输入内容、点击等操作
- page:页面对象 -- 一般根据页面的操作步骤编写方法
- scripts:测试脚本
- data:用于存储测试数据
- log:用于存储脚本运行日志
- report:用于存储测试报告
- tool:用于存储工具类
- image:用于存储失败截图
(3)base结构的搭建
①base封装思路
- 根据用例执行业务步骤,分析并提取页面公共方法
②mt登录业务所需提取的公共方法
- 初始化(driver) -- 其他公共方法需要使用driver
- 查找 元素封装
- 输入 方法封装
- 点击 元素封装
- 获取 元素文本
【base模块实现】
1 """ 2 base的封装思路:根据用例执行业务步骤分析页面公共方法,并将这些公共方法进行抽取 3 4 本项目所需公共提取的方法有: 5 -初始化 6 -查找页面元素方法 7 -输入方法 8 -点击元素 9 -获取元素文本 10 """ 11 12 from selenium.webdriver.support.wait import WebDriverWait 13 import allure 14 from tools.get_log import GetLog 15 16 log = GetLog.get_logger() 17 18 class Base: 19 # 初始化 20 def __init__(self,driver): 21 log.info("正在初始化driver:{}".format(driver)) 22 """解决driver""" 23 self.driver = driver 24 25 # 查找 方法封装 26 def base_find(self,loc,timeout=35,poll=0.5): 27 """ 28 29 :param loc: 格式为列表或元组 内容:元素定位信息,使用By类 30 :param timeout: 查找元素超时时间 默认 30秒 31 :param poll: 查找元素频率 默认为 0.5秒 32 :return: 元素 33 """ 34 log.info("正在查找元素:{}".format(loc)) 35 return (WebDriverWait(driver=self.driver, 36 timeout=timeout, 37 poll_frequency=poll).until(lambda x: x.find_element(*loc))) # 匿名函数参数x的值为driver 38 39 # 输入 方法封装 40 def base_input(self,loc,value): 41 """ 42 43 :param loc: 元素定位信息 44 :param value: 要输入的值 45 """ 46 # 1.获取元素 47 el = self.base_find(loc) 48 # 2.清空操作 49 log.info("正在对:{}元素执行清空操作".format(loc)) 50 el.clear() 51 # 3.输入操作 52 log.info("正在对:{}元素执行输入:{}操作".format(loc,value)) 53 el.send_keys(value) 54 55 # 点击 方法封装 56 def base_click(self,loc): 57 """ 58 59 :param loc: 元素定位信息 60 """ 61 log.info("正在对:{}元素执行单击操作".format(loc)) 62 # 获取元素并点击操作 63 self.base_find(loc=loc).click() 64 65 # 获取 元素文本 66 def base_get_text(self,loc): 67 """ 68 69 :param loc: 元素定位信息 70 :return: 返回元素的文本值 71 """ 72 log.info("正在对:{}元素获取文本操作,获取的文本值为:{}".format(loc,self.base_find(loc=loc).text)) 73 return self.base_find(loc=loc).text 74 75 # 截图 并写入报告 76 def base_get_img(self): 77 log.error("断言出错,正在执行截图操作") 78 # 1.调用截图的方法 79 self.driver.get_screenshot_as_file("./image/err.png") # 此时的路径以配置文件的路径为基准 80 log.error("断言出错,正在将错误图片写入allure报告") 81 # 2. 将失败截图添加到allure报告 82 allure.attach(self.driver.get_screenshot_as_png(),"失败截图",allure.attachment_type.PNG)
【webbase模块实现】
1 from base.base import Base 2 from selenium.webdriver.common.by import By 3 from time import sleep 4 5 class WebBase(Base): 6 """ 7 以下为web项目的专属方法 8 """ 9 10 # 根据显示文本点击指定元素 11 def web_base_click_element(self,placeholder_text,click_text): 12 # 1. 点击父选框 == 元素定位 + 元素操作 13 loc = (By.CSS_SELECTOR,"[placeholder='{}']".format(placeholder_text)) # 分号不能省略 14 self.base_click(loc) 15 # 2. 暂停,等待加载 16 sleep(1) 17 # 3. 点击包含显示文本的元素 == 元素点位+元素操作 18 loc = (By.XPATH,"//*[text()='{}']".format(click_text)) # 分号不可省略 19 self.base_click(loc)
【说明】
1、关于base包下各模块命名的统一规定:
①模块名编写建议:base.py
②类名编写建议:采用“大驼峰”命名规则,直接沿用模块名称,模块名中如有下划线则需要去掉下划线
③函数名编写建议:base_动词+[名词],如base_find,base_input,base_click,base_get_text等
2、在进行元素查找时,需要用到等待机制,常见的元素等待有:(详情参考:https://www.cnblogs.com/yif930916/p/14792915.html)
- 显示等待
- 隐式等待
- 强制等待
在本案例中,在进行元素查找时采用的是显示等待,查询超时时间默认为30秒,查找元素的频率默认为0.5秒;
3、输入方法封装的流程:
- 获取元素
- 清空操作 -- 主要用于清空输入框中默认的文本内容
- 输入操作(参数)
4、在获取元素文本信息时,需要return。
(4)page目录结构搭建以及page统一入口类函数的编写
①page封装思路
- 根据页面业务的操作步骤,将每步操作进行单独封装以及业务组合调用方法;
②mt登录page页面封装方法
- 输入 用户名
- 输入 密码
- 点击 登录按钮
- 获取 昵称方法 -- 断言时使用
- 组合业务方法 -- 将输入用户名、输入密码和点击登录按钮操作步骤进行组合即可
【page_mt_login模块实现】
1 """ 2 根据用例业务操作步骤,将每步操作进行单独封装及业务组合调用方法 3 4 - 输入用户名 5 - 输入验证码 6 - 点击登录按钮 7 - 获取用户昵称 8 - 组合业务方法(将上述上个步骤进行组合) 9 """ 10 from base.web_base import WebBase 11 import page 12 from time import sleep 13 from tools.get_log import GetLog 14 15 log = GetLog.get_logger() 16 17 class PageMtLogin(WebBase): 18 19 # 输入用户名 20 def page_input_username(self,username): 21 # 调用父类中的输入方法 22 self.base_input(page.mt_username,username) 23 24 # 输入验证码 25 def page_input_code(self,code): 26 # 调用父类中的输入方法 27 self.base_input(page.mt_code,code) 28 29 # 点击登录按钮 30 def page_click_login_button(self): 31 sleep(1) 32 # 调用父类中的点击方法 33 self.base_click(page.mt_login_btn) 34 35 # 获取用户昵称 -- 测试脚本层断言使用 36 def page_get_nickname(self): 37 # 调用父类中获取文本的方法 38 return self.base_get_text(page.mt_nickname) 39 40 # 组合业务方法 --> 测试脚本层调用 41 def page_mt_login(self,username,code): 42 log.info("正在登陆头条的登录业务方法,用户名:{} 验证码:{}".format(username,code)) 43 # 调用相同页面操作步骤,跨页面暂时不用考虑 44 self.page_input_username(username) 45 self.page_input_code(code) 46 self.page_click_login_button() 47 48 # 组合业务方法 --> 发布文章依赖使用 49 def page_mt_login_success(self, username='13812345678', code='246811'): 50 log.info("正在登陆头条的登录业务方法,用户名:{} 验证码:{}".format(username, code)) 51 # 调用相同页面操作步骤,跨页面暂时不用考虑 52 self.page_input_username(username) 53 self.page_input_code(code) 54 self.page_click_login_button()
【说明】
1、关于page包下各模块命名的统一规定
①模块名编写建议:page_mt_模块名,如page_mt_login.py
②类名编写建议:采用“大驼峰”命名规则,直接沿用模块名称,模块名中如有下划线则需要去掉下划线
③函数名编写建议:page_动词_名词,如page_input_username
2、关于元素配置信息的整理
- 为了方便元素定位,我们将元素配置信息存放在page包下的__init__.py模块,方便调用和统一管理 -- 在读取__init__.py的配置信息时,仅需使用 包名.变量 即可调用
- 在进行元素定位,一般采用css选择器定位和xpth定位,但是在选择上优选选用css选择器定位
以下为整个待测试功能模块所涉及的元素配置信息
1 """ 2 对页面元素定位进行统一管理,可以直接通过包名进行调用 3 css选择定位 >> xpath定位 4 5 """ 6 7 from selenium.webdriver.common.by import By 8 9 # 以下为自媒体、后台管理url 10 url_mt = 'http://ttmp.research.itcast.cn/#/login' 11 url_ht = 'http://ttmis.research.itcast.cn/#/' 12 13 14 # 以下为头条前台模块配置数据 15 # 用户名 16 mt_username = (By.CSS_SELECTOR,'input[placeholder="请输入手机号"]') 17 # 验证码 18 mt_code = (By.CSS_SELECTOR,'input[placeholder="验证码"]') 19 # 登录按钮 20 mt_login_btn = (By.CSS_SELECTOR,'.el-button--primary') # 属性中如果待空格,只用空格以后的内容即可 21 # 昵称 22 mt_nickname = (By.CSS_SELECTOR,'.user-name') 23 24 # 内容管理 25 mt_content_manage = (By.XPATH,'//span[text()="内容管理"]/..') 26 # 发布文章 27 mt_publish_article = (By.XPATH,'//*[contains(text(),"发布文章")]') 28 # 文章标题 29 mt_title = (By.CSS_SELECTOR,'[placeholder="文章名称"]') 30 # iframe 31 mt_iframe = (By.CSS_SELECTOR,'#publishTinymce_ifr') 32 # 文章内容 定位到body 勿定位到p 33 mt_content = (By.CSS_SELECTOR,'#tinymce') 34 # 封面 35 mt_cover = (By.XPATH,'//*[text()="自动"]') 36 # 频道 -- 已在web_base中实现 37 # 发表 38 mt_submit = (By.XPATH,'//*[text()="发表"]/..') 39 # 结果 40 mt_result = (By.XPATH,'//*[contains(text(),"新增文章成功")]') 41 42 43 # 以下为头条后台模块配置数据 44 # 用户名 45 ht_username = (By.CSS_SELECTOR,'[name="username"]') 46 # 密码 47 ht_pwd = (By.CSS_SELECTOR,'[name="password"]') 48 # 登录按钮 49 ht_login_btn = (By.CSS_SELECTOR,'#inp1') 50 # 昵称 51 ht_nickname = (By.CSS_SELECTOR,'.user_info')
③统一入口类及函数的编写
编写统一入口类的目的为了方便对page对象的管理,其中方法的作用主要是实例并返回各个page对象
""" Page类的统一入口:主要用于xxxPage类的管理 """ from page.page_mt_login import PageMtLogin from page.page_mt_article import PageMtArticle from page.page_ht_login import PageHtLogin from page.page_ht_audit import PageHtAudit class PageIn: def __init__(self,driver): self.driver = driver # 获取PageMtLogin对象 def page_get_PageMtLogin(self): return PageMtLogin(self.driver) # 获取PageMtArtilce对象 def page_get_PageMtArticle(self): return PageMtArticle(self.driver) # 获取PageHtLogin对象 def page_get_PageHtLogin(self): return PageHtLogin(self.driver) # 获取PageHtAudit对象 def page_get_PageHtAudit(self): return PageHtAudit(self.driver)
(5)测试业务层结构搭建
①、测试业务层常用方法
- 初始化
- 获取driver -- 通过工具类进行获取
- 通过统一入口类获取页面对象,在前端页面登录模中,主要获取PageMtLogin对象
- 结束(销毁)
- 关闭浏览器 -- 通过工具类关闭浏览器
- 测试业务方法
【get_driver.py模块的实现】
1 from selenium import webdriver 2 class GetDriver: 3 # 1. 声明变量--私有变量 4 __web_driver = None 5 6 # 2. 获取driver方法 7 @classmethod 8 def get_web_driver(cls,url): 9 # 判断driver是否为空 10 if cls.__web_driver is None: # 进行判断的目的是为了保证多次调用driver方法,返回的是同一对象 11 # 获取浏览器 12 cls.__web_driver = webdriver.Chrome() 13 # 最大化浏览器 14 cls.__web_driver.maximize_window() 15 # 打开url 16 cls.__web_driver.get(url) 17 # 返回driver 18 return cls.__web_driver 19 20 # 3. 退出driver方法 21 @classmethod 22 def quit_web_driver(cls): 23 # 判断driver不为空 24 if cls.__web_driver: 25 # 退出操作 26 cls.__web_driver.quit() 27 # 置空操作(重点) 28 cls.__web_driver = None # 关闭driver必须置空操作
关于get_driver.py模块的几点说明:
- driver为类属性,设置之前必须判断是否为空,目的是为了保证多次调用方法,返回同一对象
- 关闭driver必须进行置空操作,因为driver在执行quit方法后对象地址保留,不为空
- 为了下条用例获取driver能正常获取对象,必须置空
②测试脚本的实现(包括断言和参数化)-- 以前端登录模块
-- 在调试测试脚本的过程中,可以暂时不用断言和参数化。当测试脚本调试通过后,再添加参数化和断言
【说明】
- 由于pytest在执行测试脚本时是按照测试脚本名称的ASCII顺序执行的,因此我们在给测试脚本命名时,除了以test开头外,可以人为的添加_a_、_b_等字母,使测试脚本按预期的顺序执行
- 测试脚本的名称中千万不要出现数字,否则测试脚本无法被正常执行
from tools.get_driver import GetDriver from tools.read_yaml import read_yaml from tools.get_log import GetLog from page.page_in import PageIn import page import pytest log = GetLog.get_logger() class TestMtLogin: # 初始化 def setup_class(self): # 1. 获取driver driver = GetDriver.get_web_driver(page.url_mt) # 2. 通过统一入口路径获取PageMtLogin对象 self.mt = PageIn(driver).page_get_PageMtLogin() # 结束 def teardown_class(self): GetDriver.quit_web_driver() # 测试业务方法 # 参数化 @pytest.mark.parametrize('username,code,expect',read_yaml("mt_login.yaml")) def test_mt_login(self,username,code,expect): # 调用业务方法 self.mt.page_mt_login(username=username,code=code) try: # 断言 assert expect == self.mt.page_get_nickname() except Exception as e: # 输出错误信息 # print("错误原因:", e) log.error("断言失败,错误信息为:{}".format(e)) # 截图 self.mt.base_get_img() # 执行截图操作 # 抛出异常 raise pass
【补充说明】
在编写测试脚本的过程中,由于需要使用到参数化,因此我们可以将测试数据存放在data目录下进行统一管理,通过编写相应的工具实现对测试数据的读取。
在本项目中,采用yaml文件存放测试数据。
测试数据的读取
读取测试数据完整代码如下:
1 # 用于读取yaml中的数据信息 2 3 import os 4 from config import BASE_PATH 5 import yaml 6 7 # 用于读取yaml中的数据 8 def read_yaml(filename): 9 file_path = BASE_PATH + os.sep + 'data' + os.sep + filename # os.sep表示'\' 10 # 定义空列表,组装测试数据 11 arr = [] 12 # 获取文件流 13 with open(file_path,'r',encoding="utf-8") as f: # 读取文件注意指定编码方式 14 # 遍历 调用yaml.safe_load(f).values() 15 for datas in yaml.safe_load(f).values(): 16 #print("hello") # 此时循环只遍历一次 17 arr.append(tuple(datas.values())) 18 # 返回结果 19 return arr 20 21 if __name__ == '__main__': 22 print(read_yaml("mt_login.yaml"))
在编写完测试脚本之后,接下来我们编写pytest.ini配置文件,然后执行测试脚本
关于配置文件的几点说明:
- 位置:必须放置项目的根目录下
- 配置文件的名称:必须为pytest.ini
pytest.ini配置文件详情如下所示:
1 [pytest] 2 addopts = -s --alluredir report 3 testpaths = ./scripts 4 python_files = test*.py 5 python_classes = Test* 6 python_functions = test*
当断言出错时,为了方便分析,我们可以在测试脚本(断言)、paga页面(组合业务方法)以及Base类中添加日志记录,通过测试脚本的执行过程进行相应的分析。
import logging import logging.handlers import os from config import BASE_PATH class GetLog: # 定义类属性,保存日志 __logger = None @classmethod def get_logger(cls): # 判断日志是否为空 if cls.__logger is None: # 为空,则创建日志器 cls.__logger = logging.getLogger('myLog_mt') # 修改默认级别 -- INFO cls.__logger.setLevel(logging.INFO) # 创建处理器 -- 将日志信息输出到指定的文件中 log_path = BASE_PATH + os.sep + "log" + os.sep + "mt.log" th = logging.handlers.TimedRotatingFileHandler(filename=log_path, when="midnight", interval=1, backupCount=3, encoding="utf-8") # 创建格式器 fmt = "%(asctime)s %(levelname)s [%(filename)s(%(funcName)s:%(lineno)d)] - %(message)s" fm = logging.Formatter(fmt) # 将格式器设置到处理器中 th.setFormatter(fm) # 将处理器添加到格式器中 cls.__logger.addHandler(th) # 返回日志器 return cls.__logger if __name__ == '__main__': log = GetLog.get_logger() log.info("测试信息日志级别") log.error("测试错误级别")
(固定写法)
③测试脚本的执行以及在线生成测试报告
- 测试脚本的执行:在Terminal中输入pytest
- 在线生成测试报告:allure serve report
【其他】
- 为防止使用pytest指令执行测试脚本时出现路径报错的问题,我们可以在__init__.py文件中添加如下配置信息:
""" 用于解决路径报问题: 用于自动加载__init__.py文件所在目录下的模块 """ import sys import os sys.path.append(os.getcwd())
【报错信息如下】
- 在读取或存储相关数据到本项目下的文件夹中时(如读取测试数据/日志存储到指定目录下),由于由于路径中存在相同的前缀,因此我们可以将相同的前缀使用常量BASE_PATH进行统一管理,可以简化路径的书写(在项目根目录下的config.py模块中实现)
# 用于定义Base_path,即根目录 import os BASE_PATH = os.path.dirname(__file__)
至此,我们实现了Web自动化测试框架的搭建,当我们需要对其他模块的功能进行自动化测试时,只需要遵循如下原则:
- 首先判断页面中是否有可以提取的公共方法:如有,需要在Base中添加;如无,则继续沿用已有的Base类;
- 然后根据page页面的操作步骤,完成page页面结构的搭建; -- 具体的方法可以暂时不用实现,使用pass占位即可
- 针对page页面操作中所涉及的元素,在page > __init__.py中完成页面元素信息的配置;
- 页面结构中方法的实现;
- 在统一入口类中管理新增加的page页面;
- 针对新增加的页面,进行测试脚本的实现以及初运行和bug调试;
- 测试脚本调试通过之后,添加参数化和断言以及相应的日志记录;
- 测试脚本的执行;
- 在线生成测试报告。