Lego-美团接口自动化测试实践
智能支付稳定性测试实战
大众点评App的短视频耗电量优化实战
“小众”之美——Ruby在QA自动化中的应用
美团点评云真机平台实践
质量运营在智能支付业务测试中的初步实践
众所周知,接口自动化测试有着如下特点:
如何做好一个接口自动化测试项目呢?
我认为,一个“好的”自动化测试项目,需要从“时间”、“人力”、“收益”这三个方面出发,做好“取舍”。
不能由于被测系统发生一些变更,就导致花费了几个小时的自动化脚本无法执行。同时,我们需要看到“收益”,不能为了总想看到100%的成功,而少做或者不做校验,但是校验多了维护成本一定会增多,可能每天都需要进行大量的维护。
所以做好这三个方面的平衡并不容易,经常能看到做自动化的同学,做到最后就本末倒置了。
想要提高ROI(Return On Investment,投资回报率),我们必须从两方面入手:
我们需要做到:
我们需要做到:
所以,我这边开发了Lego接口测试平台,来实现我对自动测试想法的一些实践。先简单浏览一下网站,了解一下大概是个什么样的工具。
Lego接口测试解决方案是由两部分组成的,一个就是刚刚看到的“网站”,另一个部分就是“脚本”。
下面就开始进行“脚本设计”部分的介绍。
Lego接口自动化测试脚本部分,使用很常见的Jenkins+TestNG的结构。
相信看到这样的模型并不陌生,因为很多的测试都是这样的组成方式。
将自动化测试用例存储至MySQL数据库中,做成比较常见的“数据驱动”做法。
很多团队也是使用这样的结构来进行接口自动化,沿用的话,那在以后的“推广”中,学习和迁移成本低都会比较低。
首先来简单看一下目前的脚本代码:
public class TestPigeon {
String sql;
int team_id = -1;
@Parameters({"sql", "team_id"})
@BeforeClass()
public void beforeClass(String sql, int team_id) {
this.sql = sql;
this.team_id = team_id;
ResultRecorder.cleanInfo();
}
/**
* XML中的SQL决定了执行什么用例, 执行多少条用例, SQL的搜索结果为需要测试的测试用例
*/
@DataProvider(name = "testData")
private Iterator<Object[]> getData() throws SQLException, ClassNotFoundException {
return new DataProvider_forDB(TestConfig.DB_IP, TestConfig.DB_PORT,
TestConfig.DB_BASE_NAME,TestConfig.DB_USERNAME, TestConfig.DB_PASSWORD, sql);
}
@Test(dataProvider = "testData")
public void test(Map<String, String> data) {
new ExecPigeonTest().execTestCase(data, false);
}
@AfterMethod
public void afterMethod(ITestResult result, Object[] objs) {...}
@AfterClass
public void consoleLog() {...}
}
有一种做法我一直不提倡,就是把测试用例直接写在Java文件中。这样做会带来很多问题:修改测试用例需要改动大量的代码;代码也不便于交接给其他同学,因为每个人都有自己的编码风格和用例设计风格,这样交接,最后都会变成由下一个同学全部推翻重写一遍;如果测试平台更换,无法做用例数据的迁移,只能手动的一条条重新输入。
所以“测试数据”与“脚本”分离是非常有必要的。
网上很多的范例是使用的Excel进行的数据驱动,我这里为什么改用MySQL而不使用Excel了呢?
在公司,我们的脚本和代码都是提交至公司的Git代码仓库,如果使用Excel……很显然不方便日常经常修改测试用例的情况。使用MySQL数据库就没有这样的烦恼了,由于数据与脚本的分离,只需对数据进行修改即可,脚本每次会在数据库中读取最新的用例数据进行测试。同时,还可以防止一些操作代码时的误操作。
这里再附上一段我自己写的DataProvider_forDB
方法,方便其他同学使用在自己的脚本上:
import java.sql.*;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
/**
* 数据源 数据库
*
* @author yongda.chen
*/
public class DataProvider_forDB implements Iterator<Object[]> {
ResultSet rs;
ResultSetMetaData rd;
public DataProvider_forDB(String ip, String port, String baseName,
String userName, String password, String sql) throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.jdbc.Driver");
String url = String.format("jdbc:mysql://%s:%s/%s", ip, port, baseName);
Connection conn = DriverManager.getConnection(url, userName, password);
Statement createStatement = conn.createStatement();
rs = createStatement.executeQuery(sql);
rd = rs.getMetaData();
}
@Override
public boolean hasNext() {
boolean flag = false;
try {
flag = rs.next();
} catch (SQLException e) {
e.printStackTrace();
}
return flag;
}
@Override
public Object[] next() {
Map<String, String> data = new HashMap<String, String>();
try {
for (int i = 1; i <= rd.getColumnCount(); i++) {
data.put(rd.getColumnName(i), rs.getString(i));
}
} catch (SQLException e) {
e.printStackTrace();
}
Object r[] = new Object[1];
r[0] = data;
return r;
}
@Override
public void remove() {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
上面图中提到了“配置文件”,下面就来简单看一下这个XML配置文件的脚本:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
<suite name="Pigeon Api测试" parallel="false">
<test name="xxx-xxx-service">
<parameter name="sql"
value="SELECT * FROM API_PigeonCases
WHERE team_id=2
AND isRun=1
AND service=‘xxx-xxx-service‘
AND env=‘beta‘;"/>
<classes>
<class name="com.dp.lego.test.TestPigeon"/>
</classes>
</test>
<listeners>
<listener class-name="org.uncommons.reportng.HTMLReporter"/>
<listener class-name="org.uncommons.reportng.JUnitXMLReporter"/>
</listeners>
</suite>
对照上图来解释一下配置文件:
这样做有什么好处呢?
如上面的这个例子,在数据库中会查询出下面这56条测试用例,那么这个标签就会对这56条用例进行逐一测试。
<test>
标签时,可以分组展示使用多个<test>
标签来区分用例,最大的好处就是也能在最后的报告上,达到一个分组展示的效果。
由于使用了ReportNG
进行报告的打印,所以报告的展示要比TestNG自带的报告要更加美观、并且能自定义展示样式,点开能看到详细的执行过程。
如果有执行失败的用例,通常报错的用例会在最上方优先展示。
当两个团队开始使用时,为了方便维护,将基础部分抽出,各个团队的脚本都依赖这个Base包,并且将Base包版本置为“SNAPSHOT版本”。使用“SNAPSHOT版本”的好处是,之后我对Lego更新,各个业务组并不需要对脚本做任何改动就能及时更新。
当更多的团队开始使用后,比较直观的看的话是这个样子的:
每个团队的脚本都依赖于我的这个Base包,所以最后,各个业务团队的脚本就变成了下面的这个样子:
可以看到,使用了Lego之后:
mvn clean test -U -Dxml=xmlFileName
。由于,所有测试用例都在数据库所以这段脚本基本不需要改动了,减少了大量的脚本代码量。
有些同学要问,有时候编写一条接口测试用例不只是请求一下接口就行,可能还需要写一些数据库操作啊,一些参数可能还得自己写一些方法才能获取到啊之类的,那不code怎么处理呢?
下面就进入“用例设计”,我将介绍我如何通过统一的用例模板来解决这些问题。
我在做接口自动化设计的时候,会思考通用、校验、健壮、易用这几点。
在写自动化脚本的时候,都会想“细致”,然后“写很多”的检查点;但当“校验点”多的时候,又会因为很多原因造成执行失败。所以我们的设计,需要在保证充足的检查点的情况下,还要尽可能减少误报。
执行测试的过程中,难免会报失败,执行失败可能的原因有很多,简单分为4类:
那针对上面的情况:
通过这些手段,提高测试用例的健壮性,让每一条自动化测试用例都能很好的完成测试任务,真正发挥出一条测试用例的价值。
说了这么多,那我们来看一下一条Lego接口测试用例的样子。
一条Lego自动用例执行顺序大概是如下图这样:
简单区分一下各个部分,可以看到:
那上面图中提到了两个名词:
下面会先对这两个名词做一个简单的介绍。
比如一个请求需要用到的参数。
{
"sync": false,
"cityId": 1,
"source": 0,
"userId": 1234,
"productId": 00004321
}
这个例子中有个参数"productId": 00004321
,而由于测试的环境中,表单00004321很可能一些状态已经发生了改变,甚至表单已经删除,导致接口请求的失败,那么这时候,就很适合对"productId": 00004321
进行参数化,比如写成这样:
{
"sync": false,
"cityId": 1,
"source": 0,
"userId": 1234,
"productId": ${myProductId}
}
所以对“参数化”简单的理解就是:
通过一些操作,将一个“值”替换掉测试用例里的一个“替代字符”
${myProductId}
的值可以通过配置获取到:
下面我们来看一个“参数化”的实例:
(1) 首先我们在参数化维护页面中新建一个参数化,shopdealid
。
通过配置我们可以看到这个参数的值,是执行了一条SQL后,取用执行结果中DealID
字段的值。
(2) 在用例中,将需要这个表单号的地方用${shopdealid}替代。
那在编写测试用例的时候,大家可以看一下这个放大的图片,在这里的ProductID的值并不是硬代码一个固定的表单号,而是选择了刚才配置的参数化数据。
(3) 执行结果中,${shopdealid} 变为实时查询数据库的来的一个真实的表单号。
从结果中可以看到,我们的这个参数被替换成了一个有效的值,而这个值就是我们刚刚配置的那个SQL实时查询而来的。
多个测试用例使用同一个参数进行测试
如50条测试用例都使用同一个id作为参数进行测试,这时候我们需要变更这个id。
无参数化时:
测试数据过期导致测试用例执行失败
如一条用例参数需要传入Token,但是Token会因为时间问题而导致过期,这时候用例就失败了。
无参数化时:
有参数化时:
${测试用Token} = id:123
。数据库获取有效测试数据
参数中需要传入DealId作为参数,写死参数的话,如果这个DealId被修改引起失效,那这条测试用例就会执行失败。
不使用Lego时:
在Lego上的方案: - 使用参数化,实时获取sql结果,查询出一条符合条件的dealId来实现。 - 使用参数化,调用写好的“生成订单”接口用例实现,拿单号来实现。 - 前后置动作,插入一条满足条件的数据。
“前后置动作”的概念就比较好理解了:
在接口请求之前(或之后),执行一些操作
目前前后置动作支持6种类型:
这里的SQL同时支持Select操作,这里其实也是做了一些小的设计,会将查询出来的全部的结果,放入到这个全局Map中。
比如查询一条SQL得到下表中的结果:
id | name | age | number :–: | :–: | :–: | :–: | :–: 0 | 张三 | 18 | 1122 1 | 李四 | 30 | 3344
那我们可以使用下面左边的表达式,得到对应的结果:
${pre.name}
—- 得到 “张三”?${pre.age}
—- 得到 18${pre.number}
—- 得到 1122也可以用:
${pre.name[0]}
—- 得到 “张三”${pre.age[0]}
—- 得到 18${pre.number[0]}
—- 得到 1122${pre.name[1]}
—- 得到 “李四”${pre.age[1]}
—- 得到 30${pre.number[1]}
—- 得到 3344这样的设计,更加帮助在用例设计时,提供数据准备的操作。
(1) 首先我们在前后置维护页面中新建一个动作,获取库存上限未卖光团单
。
这个配置也是可以支持在线调试的,在调试中,可以看到可以使用的参数化:
(2) 在测试用例中的前置动作,添加获取库存上限未卖光团单
。
这样就可以在整个测试用例中,使用${pre.ProductID}
,来替换掉原有的数据信息。
(3) 最后请求接口,返回了执行成功 。
Q:那如果同样是获取三个参数,使用3个“参数化的Select操作”和使用1个“前置动作的Select操作”又有什么不同呢?
A: 不同在于执行时间上。 比如,我们查询最新的有效团单的“单号”“下单人”和“手机号”三个字段。 使用3个“参数化的Select操作”:可能当执行${单号}的时候得到的订单号是“10001”,但是当执行到${下单人}的时候,可能有谁又下了一单,可能取到的下单人变成了“10002”的“李四”而不是“10001”的“张三”了,最后可能“单号”“下单人”和“手机号”三个字段去的数据并非同一行的数据。 而使用“前置动作的Select操作”:就可以避免上面的问题,因为所有字段的数据是一次性查询出来的,就不会出现错位的情况。
Q : 那“参数化的Select操作”和“前置动作的Select操作”这样不同的取值时机又有什么好用之处呢?
A : 由于“前置动作”一定是接口请求前执行,“参数化”一定是用到的时候才执行这样的特性。 所以在检查点中,如果要验证一个数据库字段在经过接口调用后发生了变更,那使用“前置动作”和“参数化”同时去查询这个字段,然后进行比较,不一致就说明发生了变化。 所以根据使用场景,选择合适的参数化方式,很重要,选择对了,能大大提升测试用例的测试数据健壮性。
回到一开始的流程图,可以按照一类一类来看执行过程。
测试发起基本还是使用的Jenkins,稳定、成熟、简单、公司工具组支持,也支持从Lego的Web页面进行执行操作。
使用 @DataProvider 的方式,从DB数据库中读取测试用例,逐一执行进行测试。
在正式执行测试用例之前,会先进行一波参数替换的动作,在调用接口之后,还会执行一次参数替换动作。
参数替换后会进行前置动作的执行,然后在调用接口之后还会执行测试后动作,最后执行后置动作。
接口请求这部分就没什么好说的了,就是通过接口请求的参数,请求对应的接口,拿到返回结果。
这里的话是为了方便通用,所以要求返回的结果都是使用的String类型。这样做最大的好处就是。比如说我现在有一种新的接口类型需要接入。那只需要写一个方法能够请求到这个接口,并且拿到String类型的返回结果,就可以很快将新的接口类型接入Lego测试平台进行接口测试。
检查点部分是一条自动化测试用例的精髓,一条自动化测试用例是否能真正的发挥它的测试功能,就是看QA对这条测试用例的检查点编写是否做了良好设计。在Lego平台上,目前我拥有的检查点有6种不同的类型。
- 异常检查点
- 当返回结果为异常时,则会报错。
- 但是有时候为了做异常测试,可以将这个检查点关掉。
- 不为空检查点
- 顾名思义,当出现”“、”[]“、”{}“、null 这样的的结果,都会报错。也可以根据自己用例的实际情况关闭。
- 包含检查点
- 不包含检查点
- “包含”和“不包含”检查点是将接口的返回结果作为一个String类型来看,检查所有返回内容中是否“包含”或“不包含”指定的内容。
- 数据库参数检查点
- 顾名思义,不做过多的解释了。
- JsonPath检查点
- 这是我在Lego上设计的最具有特色的一种检查点类型。
JsonPath的基本写法是:{JsonPath语法}==value
JsonPath的语法和XPath的语法差不多,都是根据路径的方法找值。这里也是主要是针对返回结果为JSON数据的结果,进行检查。
具体的JsonPath语法可以参考:https://github.com/json-path/JsonPath
说完了“JsonPath的语法”,现在说一下“JsonPath检查点的语法”,“JsonPath检查点的语法”是我自己想的,主要针对以下几种数据类型进行校验:
(1) 字符串类型结果检验
==
!==
=
!=
例如:
{$.[1].name}==aa
:检查返回的JSON中第2个JSON的name字段是否等于aa。{$..type}==‘14‘
:检查返回的JSON中每一个JSON的name字段是否等于aa。{$.[1].type}==14 && {$.[1].orderId}==106712
:一条用例中多个检查用&&连接。{$..orderId}!==12
:检查返回的JSON中每个JSON的orderId字段是否不等于12。{$..type}=1
:检查返回的JSON中每个JSON的type字段是否包含1。{$.[1].type}!=chenyongda
:检查返回的JSON中第2个JSON的type字段是否不包含chenyongda。(2) 数值校验
=
>
>=
<
<=
例如:
(3) List结果检验
.length
.contains(param)
.get(index)
例如:
{$..value}.length=3
:检查返回的JSON中每个JSON的value字段的列表是否等于3。{$.[0].value}.length<5
:检查返回的JSON中第1个JSON的value字段的列表是否小于3。{$.[1].value}.length>4
:检查返回的JSON中第2个JSON的value字段的列表是否大于4。{$..value}.contains(‘222‘)
:检查返回的JSON中每个JSON的value字段的列表是否包含222字符串。{$.[0].value}.contains(1426867200000)
:检查返回的JSON中第1个JSON的value字段的列表是否包含1426867200000。{$.[0].value}.get(0)==‘222‘
:检查返回的JSON中第1个JSON的value字段的列表中第1个内容是否等于222。{$..value}.get(2)=‘22‘
:检查返回的JSON中每个JSON的value字段的列表中第3个内容是否包含22。(4) 时间类型处理
时间戳转日期时间字符串:.todate
例如:
{$..beginDate}.todate==2015-12-31 23:59:59
:检查返回的JSON中beginDate这个时间戳转换成日期后是否等于2015-12-31 23:59:59。检查点 | 检查点等号左边 | 期望值 | 验证效果 |
---|---|---|---|
{$.value}==“good” | [‘good’, ‘good’, ‘bad’, ‘good’] | “good” | 作为4个检查点,会拿列表里的每个对象逐一和“期望值”进行检验,每一次对比都是一个独立的检查点。 |
{$.value}==[“good”] | [‘good’, ‘good’, ‘bad’, ‘good’] | [“good”] | 作为1个检查点,作为一个整体做全量比对。 |
{$.value}==[‘a’, ‘b’] | [[‘a’, ‘b’],[‘a’, ‘b’],[‘a’, ‘b’, ‘c’]] | [‘a’, ‘b’] | 作为3个检查点,道理和1一样,列表中的数据分别和期望值做比较。 |
JsonPath中的检查支持“参数化”和“前后置动作”,所以会看到很多如:
{$.param}=‘${param}’ && {$.param}==${pre.param}
这样的检查点:
“参数化”和“前后置动作”也支持递归配置,这些都是为了能够让接口自动化测试用例写的更加灵活好用。
使用ReportNG可以打印出很漂亮的报告。
报告会自定义一些高亮等展示方式,只需要在ReportNG使用前加上下面的语句,就可以支持“输出逃逸”,可使用HTML标签自定义输出样式。
System.setProperty("org.uncommons.reportng.escape-output", "false");
当使用Jenkins执行后,通过Jenkins API 、和Base包中的一些方法,定时获取测试结果,落数据库,提供生成统计图表用。
既然打算做工具平台了,就得设计方方面面,可惜人手和时间上的不足,只能我一人利用下班时间进行开发。也算是担任了Lego平台的产品、后端开发、前端开发、运维和测试等各种角色。
Jenkins+TestNG+ReportNG+我自己开发的基本接口自动化测试Base jar包,基本上没什么太大难度。但是站点这块,在来美团之前,还真没开发过这样的工具平台,这个算是我的第一个带Web界面的工具。边Google边做,没想到不久还真的架起来了一个简易版本。
使用 Servlet + Jsp 进行开发,前端框架使用Bootstrap,前端数据使用jstl,数据库使用MySQL,服务器使用的公司的一台Beta环境Docker虚拟机,域名是申请的公司内网域名,并开通北京上海两侧内网访问权限。
功能上基本都是要满足的,界面上,虽然做不到惊艳吧,但是绝对不能丑,功能满足,但是长得一副80年代的界面,我自己都会嫌弃去使用它,所以界面上我还是花了一些时间去调整和设计。熟练以后就快多了。
目前Lego由五个不同的项目组成,分别是“测试脚本”、“Lego-web页面项目”、“用于执行接口测试的base包”、“小工具集合Lego-kit”和“lego-job”,通过上图可以看出各项目间的依赖关系。
细化各个项目的功能,就是下图:
简单来说,网站部分和脚本是分离的,中间的纽带是数据库。所以,没有网站,脚本执行一点问题也没有;同样的,网站的操作,和脚本也没有关系。
每天上班来会收到这样的测试邮件,通过邮件能知道昨晚执行的情况。如果有报错,可以点击“详细报告链接”,跳转到在线报告。
在现报告可以直接看到执行报错的信息,然后点击“LEGO维护传送门”,可以跳转到Lego站点上,进行用例维护。
跳转到站点上以后,可以直接展示出该条测试用例的所有信息。定位,维护、保存,维护用例,可以点击“执行”查看维护后的执行结果,维护好后“保存”即可。
仅仅3步,1~2分钟即可完成对一条执行失败的用例进行定位、调试和维护动作。
通过页面,我们就可以对一条测试用例进行:
lego-web项目同样的使用base进行的用例执行,所以执行结果和打印都与脚本执行的一致的。
为了更方便的写用例,针对部分接口开发了一键批量生成用例的小工具。
通过Jenkins接口、Base包中基础Test方法,将结果收集到数据库,便于各组对测试结果进行分析。
这是每天执行后成功率走势图:
也可以按月进行统计,生成统计的图表,帮助各个团队进行月报数据收集和统计。
有了能直观看到测试结果的图表,就会想要跟踪失败原因。
所以在成功率数据的右边,会有这样的跟踪失败原因的入口,也可以很直观地看到哪一些失败的原因还没有被跟踪。点开后可以对失败原因进行记录。
最后会有生成图表,可以很清晰地看到失败原因以及失败类型的占比。
结合Jacoco,我们可以对接口自动化的代码覆盖率进行分析。
在多台Slave机器上配置Jacoco还是比较复杂的,所以可以开发覆盖率配置辅助工具来帮助测试同学,提高效率。
除了上面的图表,还会给用例优化提供方向。
通过用例数量统计的图表,我们可以知道哪些服务用例还比较少,哪些环境的用例还比较少,可以比较有针对性的进行测试用例的补充。
通过失败原因的图表,我们可以改善自己用例中的“参数化”和“前后置动作”的使用,增加测试用例的健壮性。
通过线上接口调用量排序的图表。我们可以有效的知道优先维护哪些服务的测试用例,通过表格中,我们可以看到,哪些服务已经覆盖了测试用例,哪些没有被覆盖, 给各组的QA制定用例开发计划,提供参考。
同时在维护接口自动化测试的时候,都会看到用例评分的情况,来协助QA提高用例编写的质量。
还做了“需求白板”,用来收集使用者的需求和Bug。除此之外,Lego平台已经不只是一个接口测试的平台,还可以让想学习开发的QA领任务,学习一些开发技巧,提高自己的代码能力。
本文根据美团高级测试开发工程师勋伟在美团第43期技术沙龙“美团金融千万级交易系统质量保障之路”的演讲整理而成。主要介绍了美团智能支付业务在稳定性方向遇到的挑战,并重点介绍QA在稳定性测试中的一些方法与实践。
美团支付承载了美团全部的交易流量,按照使用场景可以将其分为线上支付和智能支付两类业务。线上支付,支撑用户线上消费场景,处理美团所有线上交易,为团购、外卖、酒店旅游等业务线提供支付能力;智能支付,支撑用户到店消费场景,处理美团所有线下交易,通过智能POS、二维码支付、盒子支付等方式,为商家提供高效、智能化的收银解决方案。其中,智能支付作为新扩展的业务场景,去年也成为了美团增速最快的业务之一。
而随着业务的快速增长,看似简单的支付动作,背后系统的复杂度却在持续提升。体现在:上层业务入口、底层支付渠道的不断丰富,微服务化背景下系统的纵向分层、服务的横向拆分,还有对外部系统(营销中心、会员中心、风控中心等)、内部基础设施(队列、缓存等)的依赖也越来越多,整条链路上的核心服务节点超过20个,业务复杂度可想而知。
此外,技术团队在短时间内就完成了从几个人到近百人规模的扩张,这也是一个潜在的不稳定因素。曾经在一段时间内,整个系统处在“牵一发而动全身”的状态,即使自身系统不做任何发版升级,也会因为一些基础设施、上下游服务的问题,业务会毫无征兆地受到影响。
痛定思痛,我们对发生过的线上问题进行复盘,分析影响服务稳定性的原因。通过数据发现,72%的严重故障集中在第三方服务和基础设施故障,对应的一些典型事故场景,比如:第三方支付通道不稳定、基础设施(如消息队列)不稳定,进而导致整个系统雪崩,当依赖方故障恢复后,我们的业务却很难立即恢复。
基于这些问题,我们开展了稳定性建设专项,目的很明确:提升服务的可用性。目标是逐步将系统可用性从2个9提升到3个9,再向4个9去努力。这个过程中最核心的两个策略:柔性可用,意思是尽可能保证核心功能可用,或在有损情况下尽可能保证核心用户体验,降低影响;另一个是快速恢复,即用工具或机制保证故障的快速定位和解决,降低故障修复时间。
围绕这两个策略,在稳定性建设中的常见操作:限流、熔断降级、扩容,用于打造系统的柔性可用;故障响应SOP、故障自动处理,用于故障处理时的快速恢复。而QA的工作更侧重于对这些“常见操作”进行有效性验证。基于经验,重点介绍“三把利剑”:故障演练、线上压测、持续运营体系。
举个真实的案例,在一次处理某支付通道不稳定的线上问题时,开发同学执行之前已经测试通过的预案(服务端关闭该通道,预期客户端将该支付通道的开关置灰,并会提示用户使用其他支付方式),但执行中却发现预案无法生效(服务端操作后,客户端该支付通道仍处于开启状态)。非故障场景下预案功能正常,故障场景下却失效了。
这就是故障演练的由来,我们需要尽可能还原故障场景,才能真正验证预案的有效性。
故障演练的整体方案,主要分为三部分:
为了更高效地开展故障演练,我们的策略是分为两个阶段进行。首先,针对单系统进行故障演练,从故障样本库出发,全面覆盖该系统所有的保护预案;在此基础上,进行全链路故障演练,聚焦核心服务故障,验证上下游服务的容错性。
事实证明,故障演练确实给我们带来了很多“惊喜”,暴露了很多隐患。这里列举三类问题:数据库主从延迟影响交易;基础设施故障时,业务未做降级;依赖服务超时设置不合理、限流策略考虑不足等。
面对业务的指数级增长,我们必须对系统可承载的流量做到心中有数。对于QA来说,需要找到精准、高效的系统容量评估方法。我们碰到的难点包括:链路长、环节多、服务错综复杂,线下环境与线上差异大等等,基于测试有效性和测试成本考虑,我们决定要做线上压测,而且要实现全链路的线上压测。
全链路压测的实现方案,与业界主流方案没有太大区别。根据压测流程,首先,场景建模,以便更真实的还原线上系统运行场景;其次,基础数据构造,应满足数据类型以及量级的要求,避免数据热点;之后,流量构建,读写流量构造或回放,同时对压测流量进行标记和脱敏;再之后,压测执行,过程中收集链路各节点的业务运行状态、资源使用情况等;最后,生成压测报告。
基于全链路线上压测方案,可以根据业务需求,灵活地进行单链路压测、分层压测等。更为重要的是,基于压测我们可以进行线上的故障演练,用于更加真实的验证系统限流、熔断等保护预案。
通过全链路线上压测,一方面让我们对系统容量做到心中有数,另一方面也让我们发现了线上系统运行过程中的潜在问题,而且这些问题一般都是高风险的。同样列举三类问题:基础设施优化,如机房负载不均衡、数据库主从延迟严重等;系统服务优化,如线程池配置不合理、数据库需要拆分等;故障预案优化,如限流阈值设置过低,有的甚至已经接近限流边缘而浑然不知等等。
智能支付的稳定性建设是作为一个专项在做,持续了近3个月的时间;在效果还不错的情况下,我们从智能支付延伸到整个金融服务平台,以虚拟项目组的方式再次运转了3个月的时间。通过项目方式,确实能集中解决现存的大部分稳定性问题,但业务在发展、系统在迭代,稳定性建设必然是一项长期的工作。于是,QA牵头SRE、DBA、RD,建立了初步的稳定性持续运营体系,并在持续完善。
下面介绍持续运营体系的三大策略:
流程规范工具化,尽可能减少人为意识因素,降低人力沟通和维护成本。
如:配置变更流程,将配置变更视同代码上线,以PR方式提交评审;代码规范检查落地到工具,尽可能将编码最佳实践抽取为规则,将人工检查演变为工具检查。
质量度量可视化,提取指标、通过数据驱动相关问题的PDCA闭环。
如:我们与SRE、DBA进行合作,将线上系统运维中与稳定性相关的指标提取出来,类似数据库慢查询次数、核心服务接口响应时长等等,并对指标数据进行实时监控,进而推进相关问题的解决。
演练压测常态化,降低演练和压测成本,具备常态化执行的能力。
如:通过自动化的触发演练报警,验证应急SOP在各团队实际执行中的效果。
基于以上三个策略,构建稳定性持续运营体系。强调闭环,从质量度量与评价、到问题分析与解决,最终完成方法与工具的沉淀;过程中,通过平台建设来落地运营数据、完善运营工具,提升运营效率。
简单展示当前持续运营体系的运行效果,包含风险评估、质量大盘、问题跟进以及最佳实践的沉淀等。
综上便是智能支付QA在稳定性建设中的重点工作。对于未来工作的想法,主要有3个方向。第一,测试有效性提升,持续去扩展故障样本库、优化演练工具和压测方案;第二,持续的平台化建设,实现操作平台化、数据平台化;第三,智能化,逐步从人工运营、自动化运营到尝试智能化运营。
美团测试团队负责App的质量保证工作,日常除了App的功能测试以外,还会重点关注App的性能测试。现在大家对手机越来越依赖,而上面各App的耗电量,直接影响了手机的待机时间,是用户非常关心的一点。本文主要通过一个典型案例,介绍App性能测试中的电量测试,并总结了我们由此引发的一些思考。
短视频作为已被市场验证的新内容传播载体,能有效增加用户停留时长。大众点评App从9.3版本开始推出短视频相关内容,在各页面新增了短视频模块。在短视频功能测试中,我们发现如果在视频列表页中播放视频,手机很快就会发烫。针对这种现象,我们马上拉取数据进行了分析,测试数据表明,视频列表页耗电量竟然是详情页的11倍。这是怎么回事儿呢?
目前行业内有很多电量测试的方法,我们采用的是Battery Historian,这是Google推出的一款Android系统电量分析工具,支持5.0(API 21)及以上系统手机的电量分析。
短视频主要包括三个核心页面:视频列表页、视频详情页、作者页,本次的测试对象就是这三个页面。
测试机型:华为Mate 9 Android 7.0 电池容量:4000mAh
播放的视频时长:1min15s 测试场景设计:WiFi环境下,打开App,播放视频,通过点击“重新播放”,连续播放10次 对比场景:停在App首页20min,手机不灭屏 注意:测试过程不充电,每次测试环境一致
如下是Battery Historian测试结果部分截图:
对测试结果数据进行汇总整理:
消耗电量:系统总电量的占比
从测试结果可以看到,短视频列表页耗电量特别高,是视频详情页的11倍。
视频列表页消耗电量过高,从测试数据可以很明显的看出来,视频列表页CPU占用时间高很多。从播放器布局来看,列表页和作者页比视频详情页只是多出了动画音符。如下图,红框中圈出的视频左下角的音符。
电量消耗差异这么大,是否跟动画音符有关呢。为了排除这个问题,重新编译了一个去掉动画音符的APK进行测试。测试结果:
从测试结果来看,CPU和耗电量很明显都下降了很多,因此确定是动画音符引起的。打开GPU视图更新的开关,查看三个页面的绘制情况。打开视频列表页,可以看到,动画音符每波动一次,会导致整个页面都在不停的绘制。如下是视频列表页绘制的情况:
从动图可以很明显看出该页面绘制十分异常,动画音符每波动一次,会导致整个页面都重新绘制一遍。
所以,到这里就明白了问题的原因,因为页面上动画音符的实现方式有问题,动画音符波动时,导致整个页面会跟着一起不停的重新绘制。而页面的重复绘制,会使App CPU占用比正常情况下高出很多,进而导致耗电量高。
定位到原因之后,开发针对性的进行了修复。动画音符柱状图的实现,之前设计由多个可变化的单柱形View组成,单个柱形View重写了onMeasure & OnDraw方法,从外部柱状图View中初始化单个柱子的高度,然后自动根据一个函数式来变化高度。因为每次都需要层层调用Measure和对应的Layout,所以造成外层控件的多次layout,进而造成CPU占用率增大。修复之后,使用另一种方式实现,只重写了View的OnDraw方法,每次使用Canvas画出所有柱状图,使用ValueAnimator来计算变化柱状图高度,也不再影响父控件的Layout。如下是修复前后的核心代码:
修复之后动画音符波动时的绘制区域:
修复之后,重新使用Battery Historian进行验证,测试结果:
从上面的测试结果,可以看到,视频列表页和作者页,耗电情况得到明显的优化。
总结一下,短视频耗电量的问题,是由于错误的绘制方法,导致CPU占用过高,进而导致耗电量高。那么因为动画音符导致耗电量异常的问题到这里就完美的解决了。CPU负载高,会导致耗电量高是显而易见的。但是还想深入探索一下,在手机系统各App耗电量排行榜中,耗电量是怎么计算的?还有哪些因素会影响耗电量呢?带着这些疑问,我们来看看系统计算耗电量的原理。
根据物理学中的知识,功=电压*电流*时间,但是一部手机中,电压值U正常来说是不会变的,所以可以忽略,只通过电流和时间就可以表示电量。模块电量(mAh)=模块电流(mA)*模块耗时(h)。模块耗时比较容易理解,但是模块电流怎样获取呢,不同厂商的手机,硬件不同,是否会影响模块的电流呢。看一下系统提供的接口:./frameworks/base/core/java/com/Android/internal/os/PowerProfile.java
该类提供了public double getAveragePower(String type)接口,type可取PowerProfile中定义的常量值,包括POWER_CPU_IDLE(CPU空闲时),POWER_CPU_ACTIVE(CPU处于活动时),POWER_WIFI_ON(WiFi开启时)等各种状态。并且从接口可以看出来,每个模块的电流值,是从power_profile.xml文件取的值。PowerProfile.java只是用于读取power_profile.xml的接口而已,后者才是存储系统耗电信息的核心文件。power_profile.xml文件的存放路径是/system/framework/framework-res.apk。
以Nexus 6P为例,在该路径获取到framework-res.apk文件。使用apktool,对framework-res.apk进行反解析,获取到手机里面的power_profile.xml文件,内容如下所示:
<?xml version="1.0" encoding="utf-8"?>
<device name="Android">
<item name="none">0</item>
<item name="screen.on">169.4278765</item>
<item name="screen.full">79.09344216</item>
<item name="bluetooth.active">25.2</item>
<item name="bluetooth.on">1.7</item>
<item name="wifi.on">21.21733311</item>
<item name="wifi.active">98.04989804</item>
<item name="wifi.scan">129.8951166</item>
<item name="dsp.audio">26.5</item>
<item name="dsp.video">242.0</item>
<item name="gps.on">5.661105191</item>
<item name="radio.active"