强行为新项目写ui自动化用例是一种什么样的体验
最近接了一个新项目,配套有个web管理后台页面,尽管需求一直在迭代以及测试时间相对不宽裕,我还是决定写点自动化用例作为功能测试的补充和回归测试的输入,顺便玩一下playwright,不在真是项目中使用一种技术其实是很难对这种技术产生深刻理解的。
项目介绍
管理后台是前后端分离的,前端用的react加上蚂蚁的前端组件库,后端是基于golang构建的微服务。其实这种项目更适合做接口测试,ui自动化作为补充就好了。
技术选型
这点很清楚,之前benchmark过,playwright比cypress性能要好,所以直接选playwright,另外playwright的python版本完全安装之后自带了pytest和一系列断言,基本上开箱即用,非常方便。另外为什么不用js而用python,主要是因为我用python写了一点接口用例,有些数据库操作的代码可以稍微复用一下,所以统一起见用python+playwright
第一个难点:登录
管理后台接的是google auth,由于我的账号开启了二次验证,需要收验证码,所以从ui上输入用户名和密码登录就走不通了。不过登录的原理基本相同,就是往cookie里写一些东西,后面所有的请求都自动带cookie到后端,后端通过之前写的那些东西就可以判断用户是谁,什么时候登录失效等。知道了原理后面就是随便试试了,我的代码大概是这样写的
def login(page):
page.goto("url")
page.evaluate(f'()=> document.cookie="{COOKIE}"')
其实就是先访问被测页面,然后自动跳到登录页面,这时候去用js设置cookie,之后再访问一次被测页面就可以自动登录了。
cookie的话可以从浏览器的开发者工具里直接拷贝出来,因为是测试环境,所以cookie的有效期很长,基本可以放心使用。
至于如何定期刷新cookie其实也不难搞,写个浏览器插件,每次打开被测页面的时候就把cookie发到一个自建后台服务,这个服务就是把cookie存到redis里,在测试用例里直接访问redis拿最新的cookie就好了。
定位有点麻烦
作为熟练工,定位对我来说应该不会是大问题,然而现在的前端组件层级嵌套厉害,html的表意性不强,而且id,name等比较有标志性的属性也不是很多,踌躇良久之后我决定请前端同学在一些关键的组件上面加上id或者name,尽管他们不是很愿意,但是我倚老卖老,还是让他们从了。
playwright的元素定位策略非常灵活,从这几天的使用情况来看,建议还是css加xpath,如果你css不熟那就直接用xpath,优点是从浏览器上就可以直接复制xpath,缺点是复制的xpath稳定性很差,页面结构稍微发生一些变化就不可用了。
最后就是playwright有录制的功能,我一般是在调试暂停的时候顺便打开录制,看看playwright自己录制的定位器是怎么样的,感觉大多数时候playwright的录制结果很靠谱,不比自己写要差。playwright生成的代码里text属性用的相对比较多点,对于多语言的系统来说需要慎用。
日期选择器
大人,大清早就亡了。
很多年前我们在处理日期选择器的时候基本上是用js来set相应的input的value值,可以做到不管ui怎么样,我想设置成什么值都可以的效果。
然而,这次时代变了。ant框架的日期选择器直接设置value值并没有什么用。我尝试过打开日期选择器选择今天的日期,因为今天的日期会高亮,所以选起来比较容易,但这样就做不到选择任意一天的效果,对于写用例来说相当不友好。
最后经过一番尝试,还是使用黑科技,模拟键盘在日期框上输入相应的日期,模拟键盘按下回车键,代码如下。
def select_time_range(self, start, end):
start_str = start.strftime('%Y-%m-%d 00:00:00')
end_str = end.strftime('%Y-%m-%d 23:59:59')
self.page.locator('#validTime').type(start_str)
self.page.locator('.ant-picker-input input >> nth=1').type(end_str)
self.page.locator('#validTime').press('Enter')
数据清理
我的用例基本建立在数据的基础上,比如我会先创建一系列的数据,然后通过确定的条件去搜索数据,再度创建数据,编辑数据等,等于是写了剧本的,每个用例按照安排运行,这就要求在所有用例执行之后做数据清理的工作,这就要求我去数据库里更新每条记录的软删除字段。另外搜索页面的数据放在es里,这就要求清理的过程中除了数据库之外还要顺便把es清掉,稍微有一点点麻烦,不过还好,几行代码的事情。
import pymysql.cursors
import requests, logging, sys
logging.basicConfig(format='%(asctime)s:%(levelname)s:%(message)s', level=logging.DEBUG)
mysql_conf = {
'host': 'xxxxx',
'user': 'test',
'port': 3306,
'db': 'xxx_db',
'password': 'secert',
}
class DBCleaner:
def __init__(self) -> None:
self.c = pymysql.connect(host=mysql_conf['host'],
user=mysql_conf['user'],
port=mysql_conf['port'],
password=mysql_conf['password'],
database=mysql_conf['db'],
cursorclass=pymysql.cursors.DictCursor)
def delete_by_mid(self, mid):
with self.c.cursor() as cursor:
select_sql = f"Select id from biz_tab where m_id = %s;"
cursor.execute(select_sql, (mid))
for row in cursor:
self.delete_es_index_by_biz_id(row['id'])
logging.info(f"Delete by mid = {mid}")
sql = f"Delete from biz_tab where m_id = %s;"
cursor.execute(sql, (mid))
self.c.commit()
def delete_es_index_by_biz_id(self, biz_id):
url = f"test_index/_doc/{biz_id}"
response = requests.request("DELETE", url).json()
logging.info(f"Deleting es index for {biz_id}, result is {response['result']}")
if __name__ == '__main__':
if len(sys.argv) == 1:
print("USAGE: db_cleaner m_id")
else:
m_id = sys.argv[-1]
cleaner = DBCleaner()
cleaner.delete_by_mid(m_id)
上面的脚本有2个作用
- 当module引入时可以调用删除的方法,传入mid就可以删除对应的记录
- 直接命令行执行时给出mid也能删除
确保清除成功
pytest在断言失败之后,后面的代码是不会被执行的,为了确保每次都会调用清理的代码,我们需要使用pytest的fixture机制,代码如下
@pytest.fixture
def data_set():
m_id = '12'
yield {'m_id': m_id}
DBCleaner().delete_by_mid(m_id)
总结
经过几天的把玩和用例编写,发现
- 迭代工程中的项目确实不太适合写ui自动化测试,不过千金难买我喜欢,我就要写
- playwright很好用
- page object还是得用
- pytest很好用
- headless很好用
- 直接写js很好用
简单来说,熟练工是第一生产力。