项目一直是手工测试为主,加上一直是TV类应用,很多自动化工具都没有针对TV类项目做很好的适配,所以只有自己动手了。主要针对项目的特殊性进行了部分改造,不一定适用于其他项目。(涉及隐私,就不提供json文件和软件名字啦)
1.非标准控件的难处
通过uiautomatorviewer获取到的不一样的磁贴,属性全部相同(除了坐标点),意味着没法通过id和class+index方式获取,text属性为空,也就没有办法通过byText的方式获取uiobject,高度定制的磁贴,让自动化很为难,如果通过坐标点,太坑爹,不能跨设备,还是坑。
2.TV类应用没有触摸操作
TV类安卓程序,主要面向的是遥控器,也就是接收的是keyevent,所以touch事件显得不这么全面,为了最接近用户,还是选择用key来做自动化。
从服务器解析json文件格式,包装成实体类CellInfo,返回一个包含磁铁信息的List。一个磁铁对应一个CellInfo,一个CellInfo需要提取的信息有
所以对应的定义如下:
package launcherClick.model;
public class CellInfo {
private String label;
private String tab;
public String getTab() {
return tab;
}
public void setTab(String tab) {
this.tab = tab;
}
private int x;
private int y;
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
}
解析用的是第三方开源库org.json,对应的解析代码如下:
package launcherClick;
import java.util.ArrayList;
import java.util.List;
import org.json.JSONArray;
import org.json.JSONObject;
import Utils.IOUtils;
import Utils.Println;
import launcherClick.model.CellInfo;
public class AMetroParse {
public static List<CellInfo> startParse(String url) {
String str = IOUtils.readFromNet(url);
if(str==null){
return null;
}
JSONObject jsonObject = new JSONObject(str);
List<CellInfo> list = new ArrayList<CellInfo>();
JSONArray _tabs = jsonObject.getJSONArray("tabs");
for(int i = 0;i<_tabs.length();i++){
String tab = _tabs.getJSONObject(i).getString("label");
JSONArray _cells = _tabs.getJSONObject(i).getJSONArray("cells");
for(int j=0;j<_cells.length();j++){
CellInfo cellInfo = new CellInfo();
cellInfo.setX(_cells.getJSONObject(j).getJSONObject("location").getInt("x"));
cellInfo.setY(_cells.getJSONObject(j).getJSONObject("location").getInt("y"));
cellInfo.setLabel(_cells.getJSONObject(j).getJSONObject("content").getString("label"));
cellInfo.setTab(tab);
list.add(cellInfo);
}
}
return list;
}
}
这部分的工作是把上一步骤解析后的实体类进行提取和处理,主要处理的内容是根据x y坐标计算磁铁移动量,更具x的极大值和分类页做跨分类移动的偏移计算以及一些其他处理。
通过x y计算偏移比较简单,只是简单计算距离:
for (CellInfo cellInfo : list) {
if (cellInfo.getLabel().contains(label)
&& cellInfo.getTab().contains(tab)) {
int x = cellInfo.getX();
int y = cellInfo.getY();
new Println("x:" + x + " " + "y:" + y + " " + "偏移量" + _offset);
// x方向
for (int i = x + _offset; i > 1; i--) {
new Println("→" + " count is " + (x + _offset));
keyRight();
}
// y方向
for (int i = y; i > 1; i--) {
new Println("↓");
keyDown();
}
}
}
恩,里面有个偏移量,跨分类用的,偏移量是通过每个分类下磁铁最大值得出来的,每一个分类最后一个磁铁的x值即为最大值,遍历当前分类下的所有磁铁的x,如果大于后面一个,则放在前面,下次再用这个值去比较下面的x值。忘记这是什么排序算法了…囧…当然,也可以用Collections的自带的算法。
private int togicOffset(String tab) {
int lineLenth = 0;
List<CellInfo> list = AMetroParse
.startParse("我是隐藏的接口");
for (CellInfo cellInfo : list) {
if (cellInfo.getTab().contains(tab)) {
int x = cellInfo.getX();
lineLenth = lineLenth >= x ? lineLenth : x;
}
}
return lineLenth;
}
那么通过上面的排序之后,就可以得到每个分类的偏移量,这样对于后面的分类磁贴,就可以知道磁贴在整体的真正位置啦,于是就可以开始移动了。
for (CellInfo cellInfo : list) {
if (cellInfo.getLabel().contains(label) && cellInfo.getTab().contains(tab)) {
int x = cellInfo.getX();
int y = cellInfo.getY();
new Println("x:" + x + " " + "y:" + y + " " + "偏移量" + _offset);
// x方向
for (int i = x + _offset; i > 1; i--) {
new Println("→" + " count is " + (x + _offset));
keyRight();
}
// y方向
for (int i = y; i > 1; i--) {
new Println("↓");
keyDown();
}
}
}
来源于monkeyrunner的思路,原理是计算两个bitmap的长宽,然后提取出里面每个像素的像素值,如果相同,相似度+1,最后再除以总像素值(比如1280x720),这样就可以把相似程度转为一个一个可以衡量的具体值了,那么判断这个界面是不是我需要点击的时候,只需要截屏当前图片和预期的图片对比,相似度达到100的时候就认为是正确的。当然也提供了局部比较的功能,比如获取不到数据的异常提示。那么代码如下,提供了多种重载的方法以及两个assert判断。
package Utils;
import junit.framework.Assert;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
public class ImageCompare {
public static void AssertBitmapEqual(Bitmap bitmap0, Bitmap bitmap1){
/** * 百分百图片相同断言-.- */
Assert.assertEquals(100, ImageCompare(bitmap0,bitmap1));
}
public static void AssertBitmapNotEqual(Bitmap bitmap0, Bitmap bitmap1){
/** * 百分之零图片相同断言 */
Assert.assertEquals(0, ImageCompare(bitmap0,bitmap1));
}
public static int ImageCompare(String path0,String path1) {
/** * 提供根据路径直接比较 */
Bitmap bitmap0 = BitmapFactory.decodeFile(path0);
Bitmap bitmap1 = BitmapFactory.decodeFile(path1);
return ImageCompare(bitmap0,bitmap1);
}
public static int ImageCompareChild(Bitmap bitmap0, Bitmap bitmap1,int x,int y,int width,int height) {
/** * 裁剪子图并比较,主要是为了解决拉取动态数据不同,但是局部提示不变的比较场景。 */
Bitmap bitmap00 = bitmap0.createBitmap(bitmap0, x, y, width, height);
Bitmap bitmap01 = bitmap1.createBitmap(bitmap1, x, y, width, height);
return ImageCompare(bitmap00,bitmap01);
}
public static int ImageCompare(Bitmap bitmap0, Bitmap bitmap1) {
/** * 比较的主函数 * 只能比较相同长宽的图片,不相等返回-1失败 * 相似度为1~100 * 原理是提取每一个像素点比较,整张图相似度取决于像素点相同个数,所以还是比较准确的 */
int picPct = 0;
int picCount = 0;
int picCountAll = 0;
new Println("begin to compare");
if (bitmap0 == null || bitmap1 == null) {
new Println("null bitmap");
return -1;
}
if (bitmap0.getWidth() != bitmap1.getWidth()
|| bitmap0.getHeight() != bitmap1.getHeight()) {
return -1;
}
new Println("宽度为:" + bitmap1.getWidth() + "高度为:" + bitmap1.getHeight());
for (int j = 0; j < bitmap1.getWidth(); j++) {
for (int i = 0; i < bitmap0.getHeight(); i++) {
if (bitmap0.getPixel(j, i) == bitmap1.getPixel(j, i)) {
picCount++;
}
picCountAll++;
}
}
int result = (int) (((float) picCount) / picCountAll * 100);
new Println(picCount + "/" + picCountAll);
new Println("相似度为:" + result);
return result;
}
}
定义了一些异常类,主要功能用于提示,这个提示的作用后面会用到。
增加了启动应用和关闭应用的方法,原理是用到了shell命令。
Runtime.getRuntime().exec("am start -n 我是包名隐藏者");
初始化方法,主要用于异常时能够一键重新开始,以及磁贴复原功能,还有其他一些小的处理就不多说啦,下面开始持续构建。
还是那句话,不持续集成的自动化不是自动化,所以这里介绍的是基于jenkins的自动化集成,其实UiAutomator做集成还是很容易的,只要把jar包放在一个固定的目录,然后shell命令执行就完事了。
然后定时任务自己选吧,构建失败的邮件提醒这里设置的是,上面我自定义的异常类,当控制台输出我的异常类,那么会认为不通过,然后触发邮件提醒。
那么至此,一个较为完整的流程就完成啦,那么在此框架上组员们(就我一个)就可以更进一部去完善二级页面的自动化用例了。