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

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

痛点

基于 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 条回复 时间 点赞
bauul #20 · 2017年11月08日 Author

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

bauul 回复

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

恒温 回复

是 appium 团队在做吗?😅

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

心向东 回复

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

15楼 已删除

不错,😄

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

我也不喜欢 appium

#8 楼 @codeskyblue +1

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

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

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

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

codeskyblue 回复

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

bauul 回复

不闪退,activity 也可能变呀

bauul #15 · 2017年11月08日 Author
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 咯

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