🎉 🎂 🍰 TesterHome 创立 6 周年纪念日 🍰 🎂 🎉

Appium [已完成] 修改 ui2.0 源码,去 session

carl · 2017年11月08日 · 最后由 carl 回复于 2017年11月14日 · 600 次阅读

痛点

基于appium,在设计修改android/ios的自动化测试用例时,
特别烦的就是在debug时,需要创建driver,就是要建立session,而建立session的动作占时又比较长,所以效率比较低
之前也尝试过使用java动态加载的方法去模拟,将测试用例或步骤放在文本文件中读取:https://testerhome.com/topics/9040
还是觉得不爽,在脚本文件中没有语法提示

我的想法

如果可以不用建立session,直接发请求,手机处理再给结果就行了,所以我想去session,
由此带来的问题:需要自己再写一个类似java-client的东西,去发这个请求

涉及改动点

  1. UI2.0 server 源码 --已修改完成,测试了,get/post请求,获取source,及查找控件均OK
  2. nodejs 源码--还没来及看,不太想动,自己写帮助类,即可

修改后的源码中的请求

private void registerPostHandler() {

register(postHandler, new FindElement("/wd/hub/element"));
register(postHandler, new FindElements("/wd/hub/elements"));
register(postHandler, new Click("/wd/hub/element/:id/click"));
register(postHandler, new Click("/wd/hub/appium/tap"));
register(postHandler, new Clear("/wd/hub/element/:id/clear"));
register(postHandler, new RotateScreen("/wd/hub/orientation"));
register(postHandler, new RotateScreen("/wd/hub/rotation"));
register(postHandler, new PressBack("/wd/hub/back"));
register(postHandler, new SendKeysToElement("/wd/hub/element/:id/value"));
register(postHandler, new SendKeysToElement("/wd/hub/keys"));
register(postHandler, new Swipe("/wd/hub/touch/perform"));
register(postHandler, new TouchLongClick("/wd/hub/touch/longclick"));
register(postHandler, new OpenNotification("/wd/hub/appium/device/open_notifications"));
register(postHandler, new PressKeyCode("/wd/hub/appium/device/press_keycode"));
register(postHandler, new LongPressKeyCode("/wd/hub/appium/device/long_press_keycode"));
register(postHandler, new Drag("/wd/hub/touch/drag"));
register(postHandler, new AppStrings("/wd/hub/appium/app/strings"));
register(postHandler, new Flick("/wd/hub/touch/flick"));
register(postHandler, new ScrollTo("/wd/hub/touch/scroll"));
register(postHandler, new MultiPointerGesture("/wd/hub/touch/multi/perform"));
register(postHandler, new TouchDown("/wd/hub/touch/down"));
register(postHandler, new TouchUp("/wd/hub/touch/up"));
register(postHandler, new TouchMove("/wd/hub/touch/move"));
register(postHandler, new UpdateSettings("/wd/hub/appium/settings"));
register(postHandler, new NetworkConnection("/wd/hub/network_connection"));
register(postHandler, new InstallApp("/wd/hub/install_app")); //增加安装app的请求,可以通过传输ftp地址或http地址来让手机自动安装应用
register(postHandler, new ExecShell("/wd/hub/exec")); //增加执行shell命令的请求
}

private void registerGetHandler() {
register(getHandler, new Status("/wd/hub/status"));
register(getHandler, new CaptureScreenshot("/wd/hub/screenshot")); //修改截屏请求,通过base64转码成string(appium原来是存在手机的sd卡中,再导出来)
register(getHandler, new GetScreenOrientation("/wd/hub/orientation"));
register(getHandler, new GetRotation("/wd/hub/rotation"));
register(getHandler, new GetText("/wd/hub/element/:id/text"));
register(getHandler, new GetElementAttribute("/wd/hub/element/:id/attribute/:name"));
register(getHandler, new GetSize("/wd/hub/element/:id/size"));
register(getHandler, new GetName("/wd/hub/element/:id/name"));
register(getHandler, new Location("/wd/hub/element/:id/location")); //修改获取location的代码,可以获取left, right, top, bottom
register(getHandler, new GetDeviceSize("/wd/hub/window/:windowHandle/size"));
register(getHandler, new Source("/wd/hub/source")); //修改获取source的源码,去掉替换node为classname的动作
register(getHandler, new Timer("/wd/hub/timer")); //增加获取手机时间的请求(appium原来的方式是通过adb命令获取的时间,默认格式,需要自己转化后使用)
register(getHandler, new LaunchApp("/wd/hub/package/:pkg/launch")); //增加打开应用的请求
}

因为是改代码,其实也简单,把sessionid都去掉,注册,取消session的动作都不要,就变成一个rest服务样子的

关于client

通过查看源码,发现在单元测试中即有此部分实现,直接复制出来,改改即可生成一个java-client,主要依赖:

<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>3.9.0</version>
</dependency>

<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20171018</version>
</dependency>

<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<version>2.4.0</version>
</dependency>
@Slf4j
public abstract class HttpUtils {
public static final MediaType JSON = MediaType.parse("application/json; " + "charset=utf-8");
private static final OkHttpClient client = new OkHttpClient();

static {
final int timeout = 15 * 1000;
client.newBuilder()
.connectTimeout(timeout, SECONDS)
.readTimeout(timeout, SECONDS)
.writeTimeout(timeout, SECONDS)
.build();
}

public static String get(final String path) {
log.debug("get:" + path);
Request request = new Request.Builder().url(path).build();
return execute(request);
}

public static String post(final String path, String body) {
Request request = new Request.Builder().url(path).post(RequestBody.create(JSON, body)).build();
log.debug("POST: path:" + path + ", body:" + body);
return execute(request);
}

private static String execute(Request request) {
String result;
try {
Response response = client.newCall(request).execute();
result = response.body().string();
} catch (IOException e) {
throw new RuntimeException(request.method() + " \"" + request.url() + "\" failed. ", e);
}
if (!request.url().toString().endsWith("screenshot")) {
log.debug("execute result:" + result);
}

return result;
}

/**
* return JSONObjects count in a JSONArray
* @param jsonArray
* @return
*/

public static int getJsonObjectCountInJsonArray(JSONArray jsonArray) {
int count = 0;
try {
for (int i = 0; i < jsonArray.length(); i++, count++) {
jsonArray.getJSONObject(i);
}
return count;
} catch (JSONException e) {
return count;
}
}

@Slf4j
public class TestUtil {

/**
* finds the element using By selector
*
* @param by
* @return
*/

public static String findElement(By by) {
JSONObject json = new JSONObject();
json = getJSon(by, json);
String result = post(UI2_SERVER_ADDR + "/element", json.toString());
log.debug("findElement: " + result);
return result;
}

/**
* waits for the element for specific time
*
* @param by
* @param time
* @return
*/

public static boolean waitForElement(By by, int time) {
JSONObject jsonBody = new JSONObject();
jsonBody = getJSon(by, jsonBody);
long start = System.currentTimeMillis();
boolean foundStatus = false;
JSONObject jsonResponse;

do {
try {
String response = post(UI2_SERVER_ADDR + "/element", jsonBody.toString());
jsonResponse = new JSONObject(response);
if (jsonResponse.getInt("status") == WDStatus.SUCCESS.code()) {
foundStatus = true;
break;
}
} catch (JSONException e) {
// Retrying for element for given time
log.error("Waiting for the element ..");
}
} while (!foundStatus && ((System.currentTimeMillis() - start) <= time));
return foundStatus;
}

/**
* waits for the element to invisible for specific time
*
* @param by
* @param time
* @return
*/

public static boolean waitForElementInvisible(By by, int time) {
JSONObject jsonBody = new JSONObject();
jsonBody = getJSon(by, jsonBody);
long start = System.currentTimeMillis();
boolean foundStatus = true;
JSONObject jsonResponse;

do {
try {
String response = post(UI2_SERVER_ADDR + "/element", jsonBody.toString());
jsonResponse = new JSONObject(response);
if (jsonResponse.getInt("status") != WDStatus.SUCCESS.code()) {
foundStatus = false;
break;
}
} catch (JSONException e) {
// Retrying for element for given time
log.error("Waiting for the element ..");
}
} while (foundStatus && ((System.currentTimeMillis() - start) <= time));
return foundStatus;
}

public static List<String> findElements(By by) {
JSONObject json = new JSONObject();
json = getJSon(by, json);
String response = post(UI2_SERVER_ADDR + "/elements", json.toString());
JSONArray elements = new JSONObject(response).getJSONArray("value");
List<String> list = new ArrayList<>();
for (int i=0; i<elements.length(); i++) {
list.add(elements.getJSONObject(i).toString());
}
return list;
}

/**
* performs click on the given element
*
* @param element
* @return
* @throws JSONException
*/

public static String click(String element) throws JSONException {
String elementId = getElementId(element);
JSONObject jsonObject = new JSONObject();
jsonObject.put("element", elementId);
return post(UI2_SERVER_ADDR + "/element/" + elementId + "/click", jsonObject.toString());
}

public static String tap(int x, int y) throws JSONException {
JSONObject jsonObject = new JSONObject();
jsonObject.put("x", x);
jsonObject.put("y", y);
return post(UI2_SERVER_ADDR + "/appium/tap", jsonObject.toString());
}

/**
* Send Keys to the element
*
* @param element
* @param text
* @return
* @throws JSONException
*/

public static String sendKeys(String element, String text) throws JSONException {
String elementId = getElementId(element);

JSONObject jsonObject = new JSONObject();
jsonObject.put("element", elementId);
jsonObject.put("text", text);
jsonObject.put("replace", false);
return post(UI2_SERVER_ADDR + "/element/" + elementId + "/value", jsonObject.toString());
}

public static String getStringValueInJsonObject(String element, String key) throws JSONException {
return new JSONObject(element).getString(key);
}

public static Object getValueInJsonObject(String jsonString, String key) throws JSONException {
return new JSONObject(jsonString).get(key);
}

/**
* get the text from the element
*
* @param element
* @return
* @throws JSONException
*/

public static String getText(String element) throws JSONException {
return getStringValueInJsonObject(get(UI2_SERVER_ADDR + "/element/" + getElementId(element) + "/text"), "value");
}

/**
* get the text from the element
*
* @param element
* @param response
*
* @return
*
* @throws JSONException
*/

// public static Response getText(String element, Response response) throws JSONException {
// String elementId = new JSONObject(element).getJSONObject("value").getString("ELEMENT");
// response = get(UI2_SERVER_ADDR + "/element/" + elementId + "/text", response);
// return response;
// }

/**
* returns the Attribute of element
*
* @param element
* @param attribute
* @return
* @throws JSONException
*/

public static String getAttribute(String element, String attribute) throws JSONException {
return get(UI2_SERVER_ADDR + "/element/" + getElementId(element) + "/attribute/" + attribute);
}

/**
* get the content-desc from the element
*
* @param element
* @return
* @throws JSONException
*/

public static String getName(String element) throws JSONException {
String response = get(UI2_SERVER_ADDR + "/element/" + getElementId(element) + "/name");
log.debug("Element name response:" + response);
return response;
}


/**
* Finds the height and width of element
*
* @param element
* @return
* @throws JSONException
*/

public static String getSize(String element) throws JSONException {
String response = get(UI2_SERVER_ADDR + "/element/" + getElementId(element) + "/size");
log.debug("Element Size response:" + response);
return response;
}

/**
* Finds the height and width of screen
*
* @return
* @throws JSONException
*/

public static Dimension getDeviceSize() throws JSONException {
String response = get(UI2_SERVER_ADDR + "/window/current/size");
log.debug("Device window Size response:" + response);
Integer height = JsonPath.compile("$.value.height").read(response);
Integer width = JsonPath.compile("$.value.width").read(response);

return new Dimension(width, height);
}

public static ElementCoordinate getElementCoordinate(String element) {
String response = getLocation(element);
Integer left = JsonPath.compile("$.value.left").read(response);
Integer right = JsonPath.compile("$.value.right").read(response);
Integer top = JsonPath.compile("$.value.top").read(response);
Integer bottom = JsonPath.compile("$.value.bottom").read(response);
return new ElementCoordinate(left, top, right, bottom);
}

public static String execShell(String cmd) {
JSONObject jsonObject = new JSONObject();
jsonObject.put("command", cmd);
return post(UI2_SERVER_ADDR + "/exec", jsonObject.toString());
}

public static void pressBack() {
JSONObject jsonObject = new JSONObject();
jsonObject.put("command", "input keyevent BACK");
post(UI2_SERVER_ADDR + "/exec", jsonObject.toString());
}


/**
* Flick on the give element
*
* @param element
* @return
* @throws JSONException
*/

public static String flickOnElement(String element) throws JSONException {
JSONObject jsonObject = new JSONObject();
jsonObject.put("element", getElementId(element));
jsonObject.put("xoffset", 1);
jsonObject.put("yoffset", 1);
jsonObject.put("speed", 1000);
String response = post(UI2_SERVER_ADDR + "/touch/flick", jsonObject.toString());
log.debug("Flick on element response:" + response);
return response;
}

/**
* Flick on given position
*
* @return
* @throws JSONException
*/

public static String flickOnPosition() throws JSONException {
JSONObject jsonObject = new JSONObject();
jsonObject.put("xSpeed", 50);
jsonObject.put("ySpeed", -180);
String response = post(UI2_SERVER_ADDR + "/touch/flick", jsonObject.toString());
log.debug("Flick response:" + response);
return response;
}

/**
* prepares the JSON Object
*
* @param by
* @param jsonObject
* @return
*/

public static JSONObject getJSon(By by, JSONObject jsonObject) {
try {
jsonObject.put("context", "");
if (by instanceof By.ByAccessibilityId) {
jsonObject.put("strategy", "accessibility id");
jsonObject.put("selector", ((By.ByAccessibilityId) by).getElementLocator());
} else if (by instanceof By.ByClass) {
jsonObject.put("strategy", "class name");
jsonObject.put("selector", ((By.ByClass) by).getElementLocator());
} else if (by instanceof By.ById) {
jsonObject.put("strategy", "id");
jsonObject.put("selector", ((By.ById) by).getElementLocator());
} else if (by instanceof By.ByXPath) {
jsonObject.put("strategy", "xpath");
jsonObject.put("selector", ((By.ByXPath) by).getElementLocator());
} else if (by instanceof By.ByAndroidUiAutomator) {
jsonObject.put("strategy", "-android uiautomator");
jsonObject.put("selector", ((By.ByAndroidUiAutomator) by).getElementLocator());
} else {
throw new JSONException("Unable to create json object: " + by);
}
} catch (JSONException e) {
log.error("Unable to form JSON Object: " + e);
}
return jsonObject;
}

/**
* prepares the JSON Object
*
* @param by
* @param contextId
* @param jsonObject
* @return
*/

public static JSONObject getJSon(By by, String contextId, JSONObject jsonObject) {
try {
jsonObject = getJSon(by, jsonObject);
jsonObject.put("context", contextId);
return jsonObject;
} catch (JSONException e) {
log.error("Unable to form JSON Object: " + e);
}
return jsonObject;
}

/**
*
* @param packageName
* @param activity
* @throws InterruptedException
*/

public static void startActivity(String packageName, String activity) throws InterruptedException {
get(UI2_SERVER_ADDR + "/wd/hub/package/"+ packageName +"/launch");
}

private static String getElementId(String element) {
if (element.contains("value")) {
return new JSONObject(element).getJSONObject("value").getString("ELEMENT");
} else {
return new JSONObject(element).getString("ELEMENT");
}
}

/**
* return the element location on the screen
*
* @param element
* @return
* @throws JSONException
*/

public static String getLocation(String element) throws JSONException {
return get(UI2_SERVER_ADDR + "/element/" + getElementId(element) + "/location");
}

/**
* performs swipe on the device screen
*
* @return
* @throws JSONException
*/

public static String swipe(int x1, int y1, int x2, int y2, int steps) throws JSONException {
// swipe from (x1,y1) to (x2,y2)
JSONObject swipeOpts = new JSONObject();
swipeOpts.put("startX", x1);
swipeOpts.put("startY", y1);
swipeOpts.put("endX", x2);
swipeOpts.put("endY", y2);
swipeOpts.put("steps", steps);

return post(UI2_SERVER_ADDR + "/touch/perform", swipeOpts.toString());
}

public static String touchDown(String element) throws JSONException {
JSONObject jsonObject = new JSONObject();
jsonObject.put("params", getJsonForElement(element).toString());
return post(UI2_SERVER_ADDR + "/touch/down", jsonObject.toString());
}

public static String touchUp(String element) throws JSONException {
JSONObject jsonObject = new JSONObject();
jsonObject.put("params", getJsonForElement(element).toString());
return post(UI2_SERVER_ADDR + "/touch/up", jsonObject.toString());
}

public static String touchMove(String element) throws JSONException {
JSONObject jsonObject = new JSONObject();
jsonObject.put("params", getJsonForElement(element).toString());
return post(UI2_SERVER_ADDR + "/touch/move", jsonObject.toString());
}

public static JSONObject getJsonForElement(String elementResponse) throws JSONException {
JSONObject jsonObject = new JSONObject();
jsonObject.put("element", getElementId(elementResponse));
return jsonObject;
}

public static boolean isElementPresent(String elementResponse) throws JSONException {
int status = new JSONObject(elementResponse).getInt("status");
return status == 0;
}
/**
* performs long click on the given element
*
* @param element
* @return
* @throws JSONException
*/

public static String longClick(String element) throws JSONException {
String elementId;
JSONObject longClickJSON = new JSONObject();
JSONObject jsonObject = new JSONObject();
try {
longClickJSON.put("params", jsonObject.put("element", getElementId(element)).put("duration",1000));
} catch (JSONException e) {
throw new RuntimeException("Element not found", e);
}
return post(UI2_SERVER_ADDR + "/touch/longclick", longClickJSON.toString());
}

/**
* perfroms scroll to the given text
*
* @param scrollToText
* @return
* @throws JSONException
*/

public static String scrollTo(String scrollToText) throws JSONException {
// TODO Create JSON object instead of below json string.Once the json is finalised from driver module
String json = " {\"cmd\":\"action\",\"action\":\"find\",\"params\":{\"strategy\":\"-android uiautomator\",\"selector\":\"" +
"new UiScrollable(new UiSelector().scrollable(true).instance(0)).scrollIntoView(new UiSelector().descriptionContains(\\\"" + scrollToText + "\\\").instance(0));" +
"new UiScrollable(new UiSelector().scrollable(true).instance(0)).scrollIntoView(new UiSelector().textContains(\\\"" + scrollToText + "\\\").instance(0));" +
"\",\"context\":\"\",\"multiple\":false}}";
JSONObject jsonObject = new JSONObject(json);
return post(UI2_SERVER_ADDR + "/touch/scroll", jsonObject.toString());
}

/**
* return the appStrings
*
* @return
* @throws JSONException
*/

public static String appStrings() throws JSONException {
JSONObject jsonObject = new JSONObject();
return post(UI2_SERVER_ADDR + "/appium/app/strings", jsonObject.toString());
}

/**
* performs screen rotation
*
* @return
* @throws JSONException
*/

public static String rotateScreen(String orientation) throws JSONException {
JSONObject postBody = new JSONObject().put("orientation", orientation);
return post(UI2_SERVER_ADDR + "/orientation", postBody.toString());
}

/**
* return screen orientation
*
* @return
* @throws JSONException
*/

public static String getScreenOrientation() throws JSONException {
String response = get(UI2_SERVER_ADDR + "/orientation");
return new JSONObject(response).get("value").toString();
}

/**
* return rotation
*
* @return
* @throws JSONException
*/

public static JSONObject getRotation() throws JSONException {
String response = get(UI2_SERVER_ADDR + "/rotation");
return new JSONObject(response).getJSONObject("value");
}

/**
* return rotation
*
* @return
* @throws JSONException
*/

public static String setRotation(JSONObject rotateMap) throws JSONException {
return post(UI2_SERVER_ADDR + "/rotation", rotateMap.toString());
}

public static String multiPointerGesture(String body) {
return post(UI2_SERVER_ADDR + "/touch/multi/perform", body);
}

public static String drag(String dragBody) throws JSONException {
return post(UI2_SERVER_ADDR + "/touch/drag", dragBody);
}

public static String captureScreenShot() {
return getStringValueInJsonObject(get(UI2_SERVER_ADDR + "/screenshot"), "value");
}

public static String getDeviceTime() {
return getStringValueInJsonObject(get(UI2_SERVER_ADDR + "/timer"), "value");
}

使用感受

终于不用每次调试的时候去创建driver了,随便打开某个页面,都可以调试了

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 19 条回复 时间 点赞
carl #1 · 2017年11月08日 作者

@Lihuazhang
社区是不是可以一起搞这事😀
或者给点建议呢

carl 回复

去 session 的其实已经有人做了。。。可以搞啊,手机端其实依赖于usb链接,本来session的诉求就不太大。

carl #3 · 2017年11月08日 作者
Lihuazhang 回复

是appium团队在做吗?😅

人家帮你管理了 session 你只要 保存一个driver 对象就好了...为什么要反过来自己搞一套...
真要搞的话 你改一下他的 client 端源码 加几个方法就行了...

carl #5 · 2017年11月08日 作者
dongdong 回复

用的是java吗,再具体点呢,加几个方法不行吧,

6楼 已删除

不错,😄

—— 来自TesterHome官方 安卓客户端

我也不喜欢appium

#8楼 @codeskyblue +1

—— 来自TesterHome官方 安卓客户端

carl #10 · 2017年11月08日 作者
codeskyblue 回复

哈哈,是的,用起来有点别扭,不过它的思想是值得学习的,改造改造😀

seesion感觉还是有存在的必要的,我写的那个uiautomator2暂时还没有session管理,但是已经计划加进来了。虽然seesion可能导致变慢,但是可以检测应用是否闪退呀。
BTW: 按照原理,session只是检测启动的应用是否存活,速度感觉也就10ms左右,不应该慢多少的

carl #12 · 2017年11月08日 作者
codeskyblue 回复

应用是否闪退也是一定需要session来判断,如果应用闪退了,activity必然改变了吧?

carl 回复

不闪退,activity也可能变呀

carl #14 · 2017年11月08日 作者
codeskyblue 回复

activity会变没错,但activity前面的包名是不会变的,另外被测应用发生闪退和ui2.0服务的会话有关系吗?我感觉没关系呢

carl 回复

没关系

carl #16 · 2017年11月09日 作者
hu_qingen 回复

胡总,带我飞啊

虽然我也不太想把session去掉,但思路我觉得可以的

dongdong 回复

在 driver 源码里 加两个方法

  1. string getSession()

2.增加构造函数 new driver(string session)

这样的话 分批次执行 只要记录上一次的session id 也能共用一个driver 了

这样修改的话 可以避免 动原本的方法代码

carl #19 · 2017年11月09日 作者
dongdong 回复

好像可以,我找时间试一下,谢谢东哥

carl #20 · 2017年11月14日 作者

不用每次都创建driver咯

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册