Android平台Robotium UI测试详解

Android平台Robotium UI测试详解

Robotium 框架工作原理及实践

Robotium 是什么

一款面向Android端的开源自动化测试框架,,Robotium是基于Instrumentation的测试框架,器测试用例的编写框架是基于Junit的
- 优势
- 同时支持Native应用和Hybrid(混合)
- 支持黑盒测试和白盒
- 基于Instrumentation,测试用例执行速度快
- 运行时识别的是控件,测试用例更健壮
- 可以通过Maven,Gradle或Ant运行
- 可以没有项目代码,只有APK文件
- 可以截图
- 缺点
- 无法跨应用
- 代码运行在被测进程,会影响性能,无法同时监控性能

Robotium 的Native使用

关于Eclipse的使用在Robotium的GitHub主页中可以找到文档,以下均是基于AndroidStudio的

要完成对手机的模拟操作,应包含以下几个基本操作:
1. 需要知道所要控件的坐标
2. 对要操作的控件进行模拟操作
3. 判断操作完成后的结果是否符合预期

1.使用 Robotium 准备工作

  1. 在需要测试modle中添加依赖

    androidTestCompile 'com.jayway.android.robotium:robotium-solo:5.6.3'
  2. AndroidStudio 在创建项目的时候,会默认的创建出androidTest路径,如果没有的话,需要在想要测试的module中的src文件夹下创建一个androidTest/java的包,然后配置module的build.gradle来指向它

    android {
        sourceSets {
        androidTest {
          java.srcDirs = ['androidTest/java']
        }
      }
    }
  3. 然后就可以写测试代码了,在java中建一个和项目根包名相同的包,我们带代码都放到这里面

2.简单的例子

写一个简单的求和功能

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:orientation="vertical"
    tools:context="com.lanou.chenfengyao.robotiumdemo.MainActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">
        <EditText
            android:id="@+id/num1_et"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:hint="a"/>
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="+"/>
        <EditText
            android:id="@+id/num2_et"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:hint="b"/>
        <TextView
            android:id="@+id/result_tv"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="=结果"/>
    </LinearLayout>
    <Button
        android:id="@+id/get_result_btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="求和"/>
</LinearLayout>

MainActivity代码

package com.lanou.chenfengyao.robotiumdemo;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {
    private EditText num1Et, num2Et;
    private Button getResultBtn;
    private TextView resultTv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        num1Et = (EditText) findViewById(R.id.num1_et);
        num2Et = (EditText) findViewById(R.id.num2_et);
        getResultBtn = (Button) findViewById(R.id.get_result_btn);
        resultTv = (TextView) findViewById(R.id.result_tv);

        getResultBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                int a = Integer.valueOf(num1Et.getText().toString());
                int b = Integer.valueOf(num2Et.getText().toString());
                //故意写错了
                resultTv.setText("= " + a + b);
            }
        });
    }
}

可以看到点击按钮的时候就会求和,并写回TextView上,可以看到我们的求和结果是故意写错了,直接这么写会是字符串的拼接而不是求和的,我们会通过测试代码来找到这个错误

测试代码-1

在androidTest/java/包名下新建一个Java class,开始写我们的测试代码,测试代码类需要继承ActivityInstrumentationTestCase2,这是测试单独Activity时需要继承的泛型填写我们需要测试的Activity类名,接下来是代码

import android.test.ActivityInstrumentationTestCase2;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;

import com.robotium.solo.Solo;

/**
 * If there is no bug, then it is created by ChenFengYao on 2017/3/3,
 * otherwise, I do not know who create it either.
 */

public class TestFirst extends ActivityInstrumentationTestCase2<MainActivity> {
    private Solo solo;

    public TestFirst() {
        super(MainActivity.class);
    }

    @Override
    protected void setUp() throws Exception {
        super.setUp();
        solo = new Solo(getInstrumentation());
        getActivity();
    }


    public void testRun() {
        EditText num1Et = (EditText) solo.getView(R.id.num1_et);
        EditText num2Et = (EditText) solo.getView(R.id.num2_et);

        Button resultBtn = (Button) solo.getView(R.id.get_result_btn);
        TextView resultTv = (TextView) solo.getView(R.id.result_tv);

        solo.enterText(num1Et,"1");
        solo.enterText(num2Et,"2");

        solo.clickOnView(resultBtn);
        solo.sleep(200);
        assertEquals("= 3",resultTv.getText().toString());
    }


}

这段代码我们复写了setUp方法,在setUp方法里我们初始化了Robotium中最重要的对象,Solo对象,几乎所有对于UI的操作都是针对这个Solo对象的,然后通过调用getActivity来启动MainActivity

下面 是testRun方法,这个方法并不是重写出来的,而是就写成这个名字,写成testXX的都能被识别,在testRun里,通过solo.getView方法来获得界面中我们所有需要的View元素,然后通过solo.enterText对EditText进行赋值,接下来调用button的点击事件,值得注意的是,测试代码并 不是运行在主线程中 的,所以我们没有办法直接操作view元素来进行更改ui,也正因为这样,我们在点击过后让solo等待一下,方便让点击事件生效,最后开始断言,textview中的文字是否是我们期望的文字,如果是的话,就证明我们的程序是没有问题的.

点击类名左边的绿色箭头就可以直接运行,也可以在运行项目那选择测试用例点击运行按钮

运行效果
可以看到程序运行后自动的填写了数组,并点击了确定,AndroidStudio的控制台也自动变成到了测试的界面,而测试页面也出现了错误日志
错误日志
第一句就告诉了我们 我们期待的结果是 = 3,而 出现的结果是 = 12,我们把程序代码改正再来运行一下

这回出现了我们期待的结果,测试就通过了

测试代码-2

Android也提供了通过注解的形式来运行测试代码,要比之前的形式更优雅一点

import android.support.test.InstrumentationRegistry;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import com.robotium.solo.Solo;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.assertEquals;

/**
 * If there is no bug, then it is created by ChenFengYao on 2017/3/3,
 * othrwise, I do not know who create it either.
 */

@RunWith(AndroidJUnit4.class)
public class TestFirst{
    private Solo solo;

    @Rule
    public ActivityTestRule<MainActivity> activityTestRule =
            new ActivityTestRule<>(MainActivity.class);

    @Before
    public void setUp() throws Exception {
        solo = new Solo(InstrumentationRegistry.getInstrumentation(),
                activityTestRule.getActivity());
    }

    @Test
    public void testRun() {
        EditText num1Et = (EditText) solo.getView(R.id.num1_et);
        EditText num2Et = (EditText) solo.getView(R.id.num2_et);

        Button resultBtn = (Button) solo.getView(R.id.get_result_btn);
        TextView resultTv = (TextView) solo.getView(R.id.result_tv);

        solo.enterText(num1Et,"1");
        solo.enterText(num2Et,"2");

        solo.clickOnView(resultBtn);
        solo.sleep(200);
        assertEquals("= 3",resultTv.getText().toString());
    }

    @After
    public void tearDown() throws Exception {
        solo.finishOpenedActivities();
    }

}

效果与之前的测试代码是一样的,就不额外演示了,与之前的代码不同,我们想要获得Activity对象则需要通过@Rule注解来获得,@Before和@After都是junit框架提供的,代表测试代码运行前和运行后,而测试方法则需要是用@Test注解进行修饰

Robotium的Native使用

控件的获取

Robotium中获取控件主要有两大方式:
- 根据被测应用的控件ID来获取
- 先获取当前页面所有的控件,对这些控件进行过滤封装后再提供相应获取控件的API

1. 根据控件的ID来获取
EditText num1Et = (EditText) solo.getView("num1_et");
EditText num2Et = (EditText) solo.getView(R.id.num2_et);

在Android中,所有的控件都会继承View,所以该方法能获取项目中所有的View,如果被测试的应用中控件有唯一ID的话,是一定能通过这种形式来获取所要操作的控件的,获取View之后再向下转型成我们需要的类型即可, 当View拥有唯一ID的时候,优先使用这种方式

2. 根据控件类型的索引,文本来获取
  1. 根据索引获取
    Robotium会先将当前界面中所有控件全部获取,然后按控件类型,索引进行过滤后获取指定的控件

    //获取页面中第一个类型是Button的控件
    Button button = (Button) solo.getButton(0);

    而基本上常用的控件Robotium都会提供这样的API,例如EditText,TextView等都是这样,但是如果是自定义组件就不行了

  2. 根据文本获取控件

    //返回界面中text属性是"登陆"的Button
    Button loginBtn = (Button) solo.getButton("登陆");

    这种方法则局限性更大,只能找到具有text属性的一些控件

  3. 根据控件类型获取控件
    Robotium可以获取 页面中/指定View下 所有的控件,或同一类型的控件

    //获取当前页面或dialog中所有的控件
    ArrayList<View> getCurrentViews();
    //获取当前页面或Dialog中所有类型为classToFilterBy的控件
    ArrayList<T> getCurrentViews(Class<T> classToFilterBy);
    //获取父控件parent下所有控件类型为classToFilterBy的控件
    ArrayList<T> getCurrentViews(class<T> classToFilterBy, View parent);
  4. 获取相同id的控件
    在遇到ListView这种组件的时候,要想对item内部的View进行操作会发现这些View无论是Id还是文本还是类型都是一致的,而ListView本身是可以很方便的获得这些item的,最简单的可以通过listView.getChildAt(index)的形式,而在获取到Item之后,我们又可以通过调用findViewById来获取该Item下的组件了

    //先获取ListView
    ListView listView = (ListView) solo.getView("main_lv");
    //获取指定的item
    View convertView =  listView.getChildAt(0);
    //从指定item中获取指定view
    TextView itemTv = (TextView) listView.findViewById(R.id.item_tv);

    这种通过findViewById的方式来获取控件,也适用于从Activity等页面中来获取指定控件.

注意:
- 当没找到想要的控件时,Robotium就会抛出运行时异常,我们可以手动捕获该异常来防止测试代码崩溃
- 尽量不适用通过索引的方式获得控件,这种方式过于依赖页面的布局结构

控件操作

对Android端控件的操作大概有点击,长按,文本输入,滑动,滚动,截图等,而为了页面真实性,有时还需要加入等待等操作

注意:
- Robotium 对控件的操作是不能够跨进程的,所以它是不能够点击到例如通知栏等区域,如果编写了这样的代码,程序是会崩溃的
- 在调试时建议打开 “指针位置” 功能,这样在编写测试代码的时候,就可以知道点击的位置坐标了

1. 点击,长按

点击和长按操作可以是找到组件然后点击该组件,而如果是自定义View时根据位置去处理手势的话,还可以根据指定的坐标去点击或者长按

//点击/长按指定的View控件
void clickOnView(View view);
void clickLongOnView(View view);
//点击/长按指定的屏幕坐标
void clickOnScreen(float x, float y);
void clickLongOnScreen(float x, float y);

Robotium 还提供了点击文本,图片的api,例如 clickOnButton(String text),它会先根据text找到Button,然后再去点击这个Button,目前Robotium也提供了点击RecyclerView的item的方法clickInRecyclerView(int itemIndex)这样的方法,来方便对ListView,RecyclerView进行操作

2. 输入操作
//在指定EditText中输入text
void enterText(EditText editText, String text);
//在指定EditText中键入文本
void typeText(EditText editText, String text);
//清空指定的输入框
void clearEditText(EditText editText);

clearEditText比较好理解,而enterText和typeText的区别是enter是直接对EditText进行赋值,而type则是像用户一样的一个一个文本的输入

EditText editText = (EditText) solo.getView(R.id.main_et);
solo.enterText(editText, "editText");
3. 滑动,滚动操作
//从起始x,y坐标滑动至终点x,y坐标,通过stepCount参数指定滑动时的步长
void drag(float fromX, float toX, float fromY, float toY, int stepCount);
//滚动到顶部/底部
void scrollToTop();
void scrollToBottom();
//向上滚动屏幕/向下滚动屏幕
void scrollUp();
void scrollDown();
//滚动至ListView第line行
void scrollListToLine(AbsListView absListView, int line);
  • 根据坐标法进行滑动的时候坐标不需要多说,而步长的意思是滑动的速度,例如从A点滑动到B点,如果步长为1,那么将直接产生从A到B的滑动手势,如果步长为100,则会将A到B之间均分成100份,然后依次滑动

  • 滚动到顶部/底部的方法是将当前屏幕滚动到顶部或底部,什么能滚,滚什么,例如:如果是ListView就会直接滚动到ListView的顶部,如果是WebView就会滚动到WebView的顶部

  • 向上滑/向下滑则也是根据当前屏幕控件来走的,基本上是滑动一屏的距离,而与drag不同的是,这些都是调用控件自身的api来进行滚动的,例如drag能触发下拉刷新等操作,但是scroll则不能,所以drag更贴近用户的操作

  • 对新的RecyclerView,同样提供了Api,但是不能指定滑动到第几行,只有向上/向下滑动,和滚动到顶端/底端,并且也只能通过RecyclerView的索引来找到RecyclerView

    boolean scrollUpRecyclerView(index);
    boolean scrollDownRecyclerView(index);
    boolean scrollRecyclerViewToTop(index);
    boolean scrollRecyclerViewToBotton(index);
4. 搜索与等待

UI自动化测试常常遇到的问题就是项目快速迭代导致界面经常变更,脚本经常出错,控件的搜索与等待则可以缓解这一问题

//休眠指定时间
void sleep(int time);
//从当前页面搜索指定文本
boolean searchText(String text);
//等待控件/文本的出现
boolean waitForView(int id);
boolean waitForText(String text);
//等待指定Activity的出现
boolean waitForActivity(String name);
//等待指定Log的出现
boolean waitForLogMessage(String logMessage);
//等待对话框的打开/关闭
boolean waitForDialogToOpen();
boolean waitForDialogToClose();
//等待某种加载条件的达成
boolean waitForCondition (Condition condition, int timeout);
//等待Fragment的加载
boolean waitForFragmentById (String id);
boolean waitForFragmentByTag (String tag);

大多数API都是比较好理解的,这个Condition是一个接口,里面只提供了一个isSatisfied()方法,该方法返回boolean类型的值,需要我们自己写的,我们可以设置一些复杂的条件来实现这个接口

solo.waitForCondition(new Condition() {
            @Override
            public boolean isSatisfied() {
                return solo.isCheckBoxChecked(0);
            }
        },200);

而几乎所有的等待操作都可以设置超时时间,需要注意的是,Robotium中查找控件,点击控件等API都默认使用了搜索与等待机制,所以非必要的情况下一般是不需要我们主动设置等待的

5. 其他操作

剩下的一些操作包括截图,对软键盘的操作,点击物理按键等等

//截图,name为图片的参数,默认路径是/sdcard/Robotium-Screenshots/
void takeScreenshot();
void takeScreenshot(String name);
//截取某段时间内一个序列
void takeScreenshotSequence(String name);
//关闭当前已打开的所有Activity
void finishOpenedActivities();
//点击返回键
void goBack();
//不断点击返回键直至返回到指定Activity
void goBackToActivity(String name);
// 收起键盘
void hideSoftKeyboard();
// 设置Activity转屏方向
void setActivityOrientation(int orientation);

在自动化测试中,当执行失败是,除了Log,最方便解决定位问题的就是运行时的截图,Robotium中提供了棘突功能,

断言

在自动化测试中,我们获取控件,执行操作后,接下来就是要对操作后的场景进行断言了,断言就是我们在程序时的一些假设,就是我们断定在程序运行到某个时候,某个属性一定是多少,我们写程序的时候,大多数都是我们事先知道输入与输出的,断言就是来测试程序的输出与我们的输入是否相符

1.Junit中的断言
//断言传入的condition参数应该为True,否则抛出一个带有message提示的Throwable异常
void assertTrue(String message, boolean condition);
// 断言传入的condition 参数应该为False, 否则抛出message的异常
void assertFalse(String message, boolean condition);
//直接认为断言失败,抛出一个message异常
void fail(String message);
// 断言相等
void assertEquals(String message,Obj o1, Obj o2);

Junit中的断言都在AndroidSdk中的 junit.framework.Assert包下的Assert类中, 常用的只有第一个,在使用的时候应该明确说明message参数的作用,方便我们后期查看

Button loginBtn = (Button) solo.getView("loginBtn");
assertTrue("登录按钮应该显示", loginBtn.isShown());
2. Robotium中的断言
// 断言当前界面是否为name参数指定的Activity,若不是则抛出一个带有message提示的Throwable异常
void assertCurrentActivity(String message, String name);
//断言不处于低内存状态
void assertMemoryNotLow();

Robotium基于Junit中的断言,也封装了几个方便在Android端自动化的时候使用,这些方法都是sole对象的方法

3.Android中的断言
//断言view在屏幕中
void assertOnScreen(View origin, View view);
//断言两个View是否低端对齐
void assertBottomAligned(View first, View second);

这些断言都是Android提供的,可以方便的判断一些Android中的UI信息他们在android.test.ViewAsserts包下,但是目前(API level 24)这个类已经被标注成Deprecated了,目前Android提供了一个ViewMatchers的类来处理View的断言,而处理的思路和以前有了变化,核心方法是

static <T> void assertThat(String message T actual, Matcher<T> matcher);

几乎所有的判断用的都是这个方法,message 是抛出的异常信息,Matcher 是断言规则,之前的在屏幕上显示,底部对齐等都变成了一种断言规则,我们也可以写自己的规则,而T则是需要的View,这里使用的是泛型,也就是该断言框架可以不仅局限于用来断言View了 也可以用来断言任何东西,例如想要判读一个TextView是否显示某种文字

TextView textView = (TextView) solo.getView(R.id.main_tv);
ViewMatchers.assertThat("显示: Welcome",textView,ViewMatchers.withText("Welcome"));

Robotium 原理

1.获取控件

我们来看一看solo对象通过view的id来获取控件的方法

public View getView(int id) {
    if(this.config.commandLogging) {
        Log.d(this.config.commandLoggingTag, "getView(" + id + ")");
    }
    return this.getView(id, 0);
}

Log先不用看,这个方法最后又调用了getView(int id, int index)的方法,那么我们再来看一看这个方法

public View getView(int id, int index) {
    if(this.config.commandLogging) {
        Log.d(this.config.commandLoggingTag, "getView(" + id + ", " + index + ")");
    }
    View viewToReturn = this.getter.getView(id, index);
    if(viewToReturn == null) {
        String resourceName = "";
        try {
            resourceName = this.instrumentation.getTargetContext().getResources().getResourceEntryName(id);
        } catch (Exception var6) {
            Log.d(this.config.commandLoggingTag, "unable to get resource entry name for (" + id + ")");
        }
        int match = index + 1;
        if(match > 1) {
            Assert.fail(match + " Views with id: \'" + id + "\', resource name: \'" + resourceName + "\' are not found!");
        } else {
            Assert.fail("View with id: \'" + id + "\', resource name: \'" + resourceName + "\' is not found!");
        }
    }
    return viewToReturn;
}

这段代码中重要的是View viewToReturn = this.getter.getView(id,index);这一句,其他的都是生成log,或者报错等,这个getter对象就是Robotium中专门用来获取View的类,那么再来看看这里面的方法

public View getView(int id, int index) {
    return this.getView(id, index, 0);
}
public View getView(int id, int index, int timeout) {
    return this.waiter.waitForView(id, index, timeout);
}
public View waitForView(int id, int index, int timeout) {
    if(timeout == 0) {
        timeout = Timeout.getSmallTimeout();
    }
    return this.waitForView(id, index, timeout, false);
}

在getView(int id, int index)方法里,又调用了 有timeout的方法重载,而在有timeout的方法重在里,又调用了waitForView,这样找到控件的方法,最后,就和等待的方法回合了,他们最后都调用了
waitForView(int id, int index, int timeout, boolean scroll)方法
一起来看一下这个方法

public View waitForView(int id, int index, int timeout, boolean scroll) {
    HashSet uniqueViewsMatchingId = new HashSet();
    long endTime = SystemClock.uptimeMillis() + (long)timeout;
    while(SystemClock.uptimeMillis() <= endTime) {
        this.sleeper.sleep();
        Iterator i$ = this.viewFetcher.getAllViews(false).iterator();
        while(i$.hasNext()) {
            View view = (View)i$.next();
            Integer idOfView = Integer.valueOf(view.getId());
            if(idOfView.equals(Integer.valueOf(id))) {
                uniqueViewsMatchingId.add(view);
                if(uniqueViewsMatchingId.size() > index) {
                    return view;
                }
            }
        }
        if(scroll) {
            this.scroller.scrollDown();
        }
    }
    return null;
}

这段代码就是最后来找到View的代码了,首先会创建出一个HashSet来存放View,接着会通过迭代器,从ViewFetcher中获取所有的View,这里传入的boolean值得意思是是否只获取可见的View,去除所有View之后,拿到他们的ID,再与我们传入的id进行对比,如果匹配成功就返回我们的View.那么拿到View的关键就是Robotium怎么获取的我们所有的View,接着一路看源码下去

static {
    try {
        String e;
        if(VERSION.SDK_INT >= 17) {
            e = "android.view.WindowManagerGlobal";
        } else {
            e = "android.view.WindowManagerImpl";
        }
        windowManager = Class.forName(e);
    } catch (ClassNotFoundException var1) {
        throw new RuntimeException(var1);
    } catch (SecurityException var2) {
        var2.printStackTrace();
    }
}

public ArrayList<View> getAllViews(boolean onlySufficientlyVisible) {
    View[] views = this.getWindowDecorViews();
    ArrayList allViews = new ArrayList();
    View[] nonDecorViews = this.getNonDecorViews(views);
    View view = null;
    if(nonDecorViews != null) {
        for(int ignored = 0; ignored < nonDecorViews.length; ++ignored) {
            view = nonDecorViews[ignored];
            try {
                this.addChildren(allViews, (ViewGroup)view, onlySufficientlyVisible);
            } catch (Exception var9) {
                ;
            }
            if(view != null) {
                allViews.add(view);
            }
        }
    }
    if(views != null && views.length > 0) {
        view = this.getRecentDecorView(views);
        try {
            this.addChildren(allViews, (ViewGroup)view, onlySufficientlyVisible);
        } catch (Exception var8) {
            ;
        }
        if(view != null) {
            allViews.add(view);
        }
    }
    return allViews;
}

public View[] getWindowDecorViews() {
    try {
        Field viewsField = windowManager.getDeclaredField("mViews");
        Field instanceField = windowManager.getDeclaredField(this.windowManagerString);
        viewsField.setAccessible(true);
        instanceField.setAccessible(true);
        Object e = instanceField.get((Object)null);
        View[] result;
        if(VERSION.SDK_INT >= 19) {
            result = (View[])((ArrayList)viewsField.get(e)).toArray(new View[0]);
        } else {
            result = (View[])((View[])viewsField.get(e));
        }
        return result;
    } catch (Exception var5) {
        var5.printStackTrace();
        return null;
    }
}

跟踪到最后发现,它通过反射拿到了一个叫WindowManagerGlobal的对象,并从这个对象中获得了顶级ViewDecorView,但是仔细观察它的反射方式,是直接通过类名构建了一个 新的WindowManagerGlobal对象 ,那么是如何从这个全新的WindowManagerGlobal中拿到DecorView的呢,只能从Android的源码入手分析了

在所有的Activity创建的过程中,我们知道都会有一个Window对象,当然真正的使用的是其子类PhoneWindow,PhoneWindow中会为Activity创建出一个DecorView对象,而DecorView是Activity的顶级View,在创建完成之后,还要通过WindowManager对象去将DecorView显示到屏幕上,这一步会调用WindowManager的makeVisible()方法

void makeVisible() {
    if (!mWindowAdded) {
        ViewManager wm = getWindowManager();            wm.addView(mDecor, getWindow().getAttributes());
        mWindowAdded = true;
    }
    mDecor.setVisibility(View.VISIBLE);
}

在这一步中,会调用WindowManagerImpl的addView方法,WindowManagerImpl就是WindowManager真正的实现类,

@Override
public void addView(View view, ViewGroup.LayoutParams params{
    mGlobal.addView(view, params, mDisplay, mParentWindow);
}

会将DecorView添加到一个叫WindowManagerGlobal的对象中,这个对象中有一个叫sDefaultWindowManager的静态的WindowManagerGlobal,所有的DecorView都会添加到这个sDefaultWindowManager的一个成员变量mViews中,它是一个专门用来存放所有的DecorView的ArrayList,同时又因为sDefaultWindowManager是静态的,所以在全局无论通过什么途径拿到它,拿到的都是同一个对象

2. 控件的操作

Robotium获取控件后,调用clickOnView(View view)方法就可以完成点击操作,这个方法可以分为两步实现
1. 根据View获取控件在屏幕中的位置
2. 根据坐标模拟点击事件

对WebView的支持

Robotium 在 4.0 开始 全面支持WebView,它支持通过ID,className等方式来获取WebElement元素

//获取当前WebView所有的WebElement元素
ArrayList<WebElement> getCurrentWebElements();
//通过By根据指定的元素获取当前WebView的所有WebElement元素
ArrayList<WebElement> getCurrentWebElements(By by);
//通过by根据指定的元素属性点击WebElement
void clickOnWebElement(By by);
//点击指定的WebElement
void clickOnWebElement(WebElement webElement);
//根据by找到WebElement,并输入指定的文本text
void enterTextInWebElement(By by, String text);
//等待根据by获得的WebElement出现
boolean waitForWebElement(By by);

By的其实就是当我们知道了Web元素的某种属性之后来通过该属性来获取控件的,例如

ArrayList<WebElement> webElements = solo.getCurrentWebElements();

ArrayList<WebElement> webElements = solo.getCurrentWebElements(By.id("example_id"));

在使用Robotium操作WebView的时候需要注意的是,它只支持原生的WebView而不支持第三方浏览器内核

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值