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

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

痛点

基于 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 条回复 时间 点赞

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

bauul 回复

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

恒温 回复

是 appium 团队在做吗?😅

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

bauul #13 · 2017年11月08日 Author
心向东 回复

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

15楼 已删除

不错,😄

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

我也不喜欢 appium

#8 楼 @codeskyblue +1

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

bauul #14 · 2017年11月08日 Author
codeskyblue 回复

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

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

bauul #12 · 2017年11月08日 Author
codeskyblue 回复

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

bauul 回复

不闪退,activity 也可能变呀

codeskyblue 回复

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

bauul 回复

没关系

CC 回复

胡总,带我飞啊

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

心向东 回复

在 driver 源码里 加两个方法

  1. string getSession()

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

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

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

心向东 回复

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

bauul #18 · 2017年11月14日 Author

不用每次都创建 driver 咯

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