最近在做 APP 性能自动化的需求,结合 Monkey 跑不靠谱,很想用思寒的工具,但是没有源码(反编译的效果不理想),对于控制欲强迫症的我只能在有限的时间重复造轮。
今天发这个帖子,主要是不想闭门造车,这里面肯定有我没考虑到的坑,希望各位这方面有经验的能给些意见和思路,避免少走弯路。
1.获取界面的布局(uiautomator dump 或者 appium getPageSource),解析后载入 clickable 为 true 的节点
2.定义 UiNode
a.id(node 唯一标识):MD5-class-locator
b.windowID(所属窗口) MD5
c.depth(窗口层级)
d.leftNode(父节点,只有 1 个)
e.rightNode(子节点,集合)
3.定义任务栈:Stack,采用先进后出原则
4.定义配置类:黑名单、遍历深度、遍历时间、引导规则(欢迎界面滑动、优先登录)
5.界面控件按默认规则操作,也支持自定义控件,以 android 为例:
标准控件:
android.widget.LinearLayout => click
android.widget.LinearLayout => click
android.widget.FrameLayout => click
android.widget.ImageView => click
android.widget.TextView => click
android.widget.Button => click
android.widget.EditText => input
android.webkit.WebView => context
自定义控件:
xxx.xxx.xxx => click
xxx.xxx.xxx => input
6.遍历步骤
a.如果设置了引导规则,优先完成欢迎界面和登录处理;
b.判断当前界面 native 还是 webview;
c.获取当前界面内容并解析;
d.识别界面是否发生迁移;当界面发生迁移时,将迁移后的界面替换为当前界面,并记录当前操作;当界面未发生迁移时,执行下一个步骤;
e.判断是否达到临界条件;当达到临界条件时,遍历结束,当未达到临界条件时,判断是否所有的界面都遍历完成;当所有的界面都遍历完成时,遍历结束;
7.动态分配端口,自动启动 Appium Server、生成思维导图、回放动画、测试报告等
1.根据当前节点获取 XPath
a.目前生成的是绝对路径,有没有生成相对路径的靠谱方法(看了几个都有问题)?
b.Android 的 node name 和 iOS 不同,有时候是控件类名,有时候是清一色的 node,是否是这样的?目前只能取 attribute 的 class value 生成
@Override
public String getXPath(Node n) {
String clazz1;
String clazz2;
NamedNodeMap nodeMap;
if (null == n)
return null;
Node parent = null;
Stack<Node> hierarchy = new Stack<Node>();
StringBuffer buffer = new StringBuffer();
hierarchy.push(n);
switch (n.getNodeType()) {
case Node.ATTRIBUTE_NODE:
parent = ((Attr) n).getOwnerElement();
break;
case Node.ELEMENT_NODE:
parent = n.getParentNode();
break;
case Node.DOCUMENT_NODE:
parent = n.getParentNode();
break;
default:
throw new IllegalStateException("Unexpected Node type" + n.getNodeType());
}
while (null != parent && parent.getNodeType() != Node.DOCUMENT_NODE) {
hierarchy.push(parent);
parent = parent.getParentNode();
}
Object obj = null;
while (!hierarchy.isEmpty() && null != (obj = hierarchy.pop())) {
Node node = (Node) obj;
boolean handled = false;
if (node.getNodeType() == Node.ELEMENT_NODE) {
Element e = (Element) node;
nodeMap = node.getAttributes();
clazz1 = getAttribute(nodeMap, "class");
if (buffer.length() == 0) {
buffer.append(clazz1);
} else {
buffer.append("/");
buffer.append(clazz1);
if (!handled) {
int prev_siblings = 1;
Node prev_sibling = node.getPreviousSibling();
while (null != prev_sibling) {
if (prev_sibling.getNodeType() == node.getNodeType()) {
nodeMap = prev_sibling.getAttributes();
clazz2 = getAttribute(nodeMap, "class");
if (clazz2.equalsIgnoreCase(clazz1)) {
prev_siblings++;
}
}
prev_sibling = prev_sibling.getPreviousSibling();
}
buffer.append("[" + prev_siblings + "]");
}
}
} else if (node.getNodeType() == Node.ATTRIBUTE_NODE) {
nodeMap = node.getAttributes();
clazz1 = getAttribute(nodeMap, "class");
buffer.append("/@");
buffer.append(clazz1);
}
}
return buffer.toString().replaceAll("null", "");
}
2.基于深度优先的探索性遍历的算法(测试用骨架)
这里只是测试用的骨架,引导规则、黑名单、遍历时间、返回逻辑等未加入。
a.大致看了下,觉得返回逻辑可能存在一些坑,有没有好的设计思路供参考?
b.如果不做遍历深度的限制,Stack是否会导致内存溢出的风险?
public class AndroidEngine implements Engine {
static Stack<UiNode> nodeStack = new Stack<UiNode>();
public static void main(String[] args) {
UiNode uiNode1 = new UiNode();
uiNode1.setId("id1");
uiNode1.setWindowID("win1");
AndroidEngine test = new AndroidEngine();
test.dfsSearch(uiNode1, 3);
}
/**
* 模拟生成子节点
*
* @param depth
* @param left
* @return
*/
public List<UiNode> getChild(int depth, UiNode left) {
UiNode uiNode;
List<UiNode> uiNodeList = new ArrayList<>();
for (int i = 2; i > 0; i--) {
uiNode = new UiNode();
uiNode.setId("id" + depth + i);
uiNode.setWindowID("win" + depth);
uiNode.setDepth(depth);
uiNode.setLeftNode(left);
uiNodeList.add(uiNode);
}
return uiNodeList;
}
@Override
public void dfsSearch(UiNode uiNode, int depth) {
UiNode refNode = null;
UiNode thisNode;
List<UiNode> children = null;
uiNode.setDepth(1);
nodeStack.push(uiNode);
while (!nodeStack.isEmpty()) {
// 出栈
thisNode = nodeStack.pop();
if (thisNode.getLeftNode() != null) {
System.out.println("left node: " + thisNode.getLeftNode().getWindowID() + " -> " + thisNode.getLeftNode().getId() + ", this node: " + thisNode.getWindowID() + " -> " + thisNode.getId());
} else {
System.out.println("left node: null, this node: " + thisNode.getWindowID() + " -> " + thisNode.getId());
}
// 遍历深度控制,0 表示未限制
if (depth == 0 || thisNode.getDepth() < depth) {
thisNode.setRightNode(getChild(thisNode.getDepth() + 1, thisNode));
children = thisNode.getRightNode();
// 入栈
if (children != null && !children.isEmpty()) {
for (UiNode child : children) {
nodeStack.push(child);
}
}
}
// 当前节点作为下一遍历节点的参照物
refNode = thisNode;
}
}
}
public class UiNode {
private boolean hasVisited = false;
private int depth;
private String id;
private String windowID;
private UiNode leftNode = null;
private List<UiNode> rightNode = null;
public int getDepth() {
return depth;
}
public void setDepth(int depth) {
this.depth = depth;
}
public String getWindowID() {
return windowID;
}
public void setWindowID(String windowID) {
this.windowID = windowID;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public UiNode getLeftNode() {
return leftNode;
}
public void setLeftNode(UiNode leftNode) {
this.leftNode = leftNode;
}
public List<UiNode> getRightNode() {
return rightNode;
}
public void setRightNode(List<UiNode> rightNode) {
this.rightNode = rightNode;
}
public boolean isHasVisited() {
return hasVisited;
}
public void setHasVisited(boolean hasVisited) {
this.hasVisited = hasVisited;
}
}
3.如何生成思维导图
@seveniruby
如果有第三方插件支持,请提供传送门;
如果没有,那么是如何实现的,请给个解题思路;