Robotium 自动化测试

Robotium 自动化测试

一、Setup

Android Studio环境下,在所要测试的Module的build.gradle文件下添加,

compile ‘com.jayway.android.robotium:robotium-solo:5.4.1’

然后Sync下。

二、Start

Robotium即是对Instrumentation框架方法的封装,所以使用之前需要继承测试类,重写构造器,setUp()和tearDown()方法。

  public class SplashActivityTest extends ActivityInstrumentationTestCase2 {

    private Solo solo;

    public SplashActivityTest() throws ClassNotFoundException {
        super(SplashActivity.class);
    }

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

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

其中继承的是ActivityInstrumentationTestCase2测试类。ActivityInstrumentationTestCase2测试类主要用于跨Activity的测试。(测试类的关系和架构见附页1)

其中
solo = new Solo(getInstrumentation());

solo.finishOpenedActivities();是Robotium框架独有的。

测试方法必须是public的,且以test开头。这是因为用的是Junit3框架。

public void testRun() {}

三、Use

我们所会用到的API主要来自于四个部分:

  • Robotium框架核心类Solo
  • Instrumentation框架下的ActivityInstrumentationTestCase2类
  • unit3下的断言方法
  • UI组件的getXX或findXX方法

这里我们只单独简单说明Solo类的API。

Solo类是Robotium中核心类,几乎所有的测试方法都是调用它的方法实现。

1.getXX() 匹配方法

一般使用getView(int id),getText(String text)匹配想要操作的组件,如果匹配不到,还可以尝试getButton(),getImage()等

// getCurrentActivity()方法返回的是界面显示的Activity。

2.action() 操作方法

  • clickOnView(),clickOnText()
  • clearEditText(),enterText()
  • clickInList(),clickInRecyclerView()
  • scrollDown(),scrollViewToSide()(如果是ListView等,这里不推荐使用,不如直接使用moveTo等方法)
  • goBack()

3.ssert()/search() 断言方法

  • assert:只有solo. assertCurrentActivity() 方法
  • search()返回布尔值,用于逻辑判断,断言。
  • searchXXX(),waitXXX() 与assertEqual() 方法配合。

4.waitXX(,time) 等待方法

waitForActivity()等,返回布尔值。

在time时间内条件成立,立即执行下一步,不一定非要等待time时间。

同时我们可以使用返回false的waitXX方法作为稳定的定时器使用,我常常用waitForEmptyActivityStack()。

三个作用:等待程序响应,逻辑判断,放缓测试速度

备注:waitXX方法的扩展:waitForCondition()用来支持所有判断条件,实例见下:

  solo.waitForCondition(new myCondition(viewGroup), 3000)

class myCondition implements Condition {

    Object viewGroup;

    myCondition(Object viewGroup) {

        this.viewGroup = viewGroup;
    }

    @Override
    public boolean isSatisfied() {

        return viewGroup != null;
    }
}

四、practice

SplashActivity.java(这里难点主要是对listView或RecyclerView组件的遍历和判断条件的选择)

需求:

1、从启动页进入

2、切换城市,遍历所有城市

3、搜索1-9

4、如果搜索结果返回多个匹配值,遍历所有匹配值。

5、如果进入站点详情,遍历所有路线,点击收藏按钮并取消收藏

6、如果进入线路详情,遍历所有站点,刷新,然后换向后,重复一遍换向前操作。

1、从启动页进入

测试进入第一个的界面即我们绑定的界面
public SplashActivityTest() throws ClassNotFoundException {
super(SplashActivity.class);
}
这里类名是不是这个类无所谓,但构造方法中调用你想要进入的类。

2、切换城市,遍历所有城市

由于城市列表是ListView或者RecyclerView,数据对我们是未知的,布局是可变的,我们不能采用直接clickOnText,或者clickOnView进入某个城市。
对于ListView或者RecyclerView解决办法有三种,具体使用看具体情况:

a、通过遍历,获得所有城市集合,然后调用clickOnText。
获得所有城市集合:

  /**
 * 在CityChooseActivity界面内得到城市列表
 *
 * @return 城市列表
 */
private String[] getCities() {

    solo.clickOnView(solo.getView(R.id.cll_mod_main_tab_mine));
    solo.clickOnView(solo.getView(R.id.cll_row_city));

    if (solo.waitForActivity(CityChooseActivity.class, 3000)) {

        solo.waitForEmptyActivityStack(3000);
        StickyListHeadersListView stickyLv =(StickyListHeadersListView)     solo.getCurrentActivity().findViewById(R.id.cll_city_change_list);

        List cities = new ArrayList();

        for (int i = 3; i < stickyLv.getAdapter().getCount(); i++) {

            City city = (City) stickyLv.getAdapter().getItem(i);

            cities.add(city.getCityName());
        }
        solo.goBack();

        return (String[]) cities.toArray(new String[cities.size()]);
    } else {
        solo.goBack();

        return new String[]{"天津", "北京"};

}

clickOnText方法有三个特点:

1)特别稳定,一般都能找到相应的view,无论view被包裹多少层。

2)如果text没有显示或者所在Item初始化,方法会使ListView滚动,直到找到该View。

当然可以设置是否滚动查找。

3)由于查找该View的机制或者存在两个以上相同Text的View,默认选择第一个,可以设置选第几个。

但是这里我们使用

solo.clickOnView(solo.getView(R.id.cll_search_section));

solo.clearEditText((EditText) solo.getView(R.id.frame_toolbar_search_query));

solo.enterText((EditText) solo.getView(R.id.frame_toolbar_search_query), city);

solo.clickInList(1);

借用程序的功能,同时测试了这个功能。

但是这种方法的缺陷一是,必须首先进入ListView所在的界面,初始化ListrView后,才能得到城市集合,否则只是一个空集合;二是有些组件无法通过遍历获得城市集合。

b、clickInList ()或者clickInRecyclerView()

解决方法1的缺陷,但缺点是:

1)方法内部的限制如果触发事件并没有绑定到Item上,二是item的子View上,可能无法触发事件。

2)方法参数为可见child的index,需要对不同界面进行position到index的转换和组件滚动。

3)如果一个Item有两个监听事件,比如站点详情的收藏,无法触发。

(注意:参数从1开始)

我在对多结果的遍历和线路详情中遍历所有路线使用这种方法,详细使用会在下文中说明。

c、通过给定的组件,通过findLastVisibleItemPosition或者findViewByPosition找到目标View,再找到具体子View,使用clickOnView(targetView);
终极方法(clickOnScreen除外),几乎可以解决所有需求,但是特别麻烦而且不稳定。不稳定在于即使position指的是从0开始的item数,但依然要求在方法执行时该item是可见的,否则出bug。
我在线路详情遍历所有站点,采用这种方法,详细使用会在下文中说明。

3、搜索1-9

4、如果搜索结果返回多个匹配值,遍历所有匹配值。

由于不同搜索内容返回的结果是不同懂得,需要对搜索结果的处理,同时考虑网络环境不佳,导致无结果返回的情况。

  for (String content : contents) {

    performSearch(content);
    Log.d("TestTab", "查询" + content);

    Log.d("TestTab", "处理返回的结果");
    if (solo.waitForActivity(LineDetailActivity.class, 2000)) {

        Log.d("TestTab", "进入线路详情界面");
        lineModule.run();

        solo.goBack();
        solo.goBack();


    } else if (solo.waitForActivity(StationDetailActivity.class, 2000)) {

        Log.d("TestTab", "进入车站详情界面");
        stationModule.run();

        solo.goBack();
        solo.goBack();


    } else if (solo.searchButton("重试", true)) {

        Log.d("TestTab", "没有返回结果");

        solo.goBack();

        continue;

    } else if (solo.searchText("没有找到合适的线路和车站", true)) {

        Log.d("TestTab", "没有找到合适的线路和车站");

        solo.goBack();

        continue;

    } else if (solo.waitForFragmentById(R.id.cll_fragment_fuzzy)) {

        traversalResults();
        solo.goBack();
    }
}

这里主要是判断条件的应用。这里需要注意有两点,一是solo.waitForFragmentById()方法,在程序里如果ViewFlipper的机制是呈现的另个ViewGroup时,目标Fragment依然被初始化。那么solo.waitForFragmentById()就无法正常起作用。
二是searchXX方法。这个方法可以穿透封装,找到目标View。但是两个缺点:

  • 由于方法内部是遍历所有View,寻找匹配项,耗时较长。
  • 在ListView等组件中,该方法会自动滚动寻找匹配项,但不会再滚动回原来位置,在调用clickInLine等方法时,需要手动回滚到第一行。

traversalResults()方法,是通过上述第二种遍历ListView的方法遍历。

  /**
 * 遍历返回多个搜索结果的情况
 */
private void traversalResults() {

    ListView lv = (ListView) solo.getView(R.id.cll_lv, 1);

    solo.scrollListToTop(lv);

    int sum = lv.getAdapter().getCount();
    Log.d("TestTab", "有" + sum + "结果");

    int itemCount = lv.getLastVisiblePosition() - lv.getFirstVisiblePosition() + 1;
    Log.d("TestTab", "可见的Item有 " + itemCount);

    int loops = sum / itemCount;
    int last = sum % itemCount;

    if (sum < itemCount + 1) {

        for (int p = 1; p < sum + 1; p++) {

            solo.clickInList(p);

            toLineOrStation();
            solo.goBack();

        }

    } else {

        for (int i = 0; i < loops + 1; i++) {

            if (i < loops) {

                for (int m = 1; m < itemCount; m++) {//由于scrollDown()逻辑和遍历条件无关,到itemCount-1便停止遍历

                    solo.clickInList(m);
                    Log.d("TestTab", "搜索结果遍历" + m);

                    toLineOrStation();
                    solo.goBack();

                }
                solo.scrollDown();

            } else {

                for (int n = itemCount - last + 1; n < itemCount + 1; n++) {

                    solo.clickInList(n);

                    toLineOrStation();
                    solo.goBack();

                }
            }
        }
    }
}

需要得到三个值item的总数,单页可见Item数,前两者相除的余数,具体逻辑看上面代码。

注意在处理最后一页的逻辑。

最后一个需要注意的地方:

ListView lv = (ListView) solo.getView(R.id.cll_lv, 1);

Index为1,这是由于当时可以匹配到两个ListView,选择第二个。

5、如果进入站点详情,遍历所有路线,点击收藏按钮并取消收藏

两个步骤,遍历点击每个Item,遍历点击每个收藏按钮

遍历点击每个Item,获得Item总数,屏幕内item数量,前两者相除的商和余数,每次循环通过clickInLine遍历屏幕上的每一个item,循环商加一的次数,最后一次只需要遍历倒数余数个item。

  @Override
public void run() {

    solo.waitForActivity(StationDetailActivity.class);//保证recyclerView不为空。

    RecyclerView recyclerView = (RecyclerView) solo.getView(R.id.cll_station_detail_list);
    //找到目标RecyclerView,这样比getCurrentActivity().findViewById()更加有效。

    LinearLayoutManager mLinearLayoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();

    int sum = recyclerView.getAdapter().getItemCount();
    Log.d("TestTab", "共有 " + sum + " 条线路");

    int itemCount = ((LinearLayoutManager) recyclerView.getLayoutManager()).findLastVisibleItemPosition() - ((LinearLayoutManager) recyclerView.getLayoutManager()).findFirstVisibleItemPosition() + 1;//计算显示在屏幕的item的数量

    int loops = sum / itemCount;需要滚动几次
    int last = sum % itemCount;最后一次

    if (sum < itemCount + 1) {

        for (int p = 0; p < sum; p++) {

            clickItem(p);

            clickFavourInStation(mLinearLayoutManager, p);

        }

    } else {

        for (int i = 0; i < loops + 1; i++) {

            if (i < loops) {
                //保证不点击到返回按钮
                recyclerView.smoothScrollToPosition(itemCount * i);//由于solo.scrollDown()不稳定,需要稳定的定位

                for (int m = 0; m < itemCount; m++) {

                    clickItem(m);

                    clickFavourInStation(mLinearLayoutManager, i * itemCount + m);
                }
                solo.scrollDown();

            } else {

                int lastPageItemsCount = ((LinearLayoutManager) recyclerView.getLayoutManager()).findLastVisibleItemPosition() - ((LinearLayoutManager) recyclerView.getLayoutManager()).findFirstVisibleItemPosition() + 1;

                for (int n = lastPageItemsCount - last, q = 0; n < lastPageItemsCount - 1; n++, q++) {

                    clickItem(n);

                    clickFavourInStation(mLinearLayoutManager, loops * itemCount + q);
                }
            }
        }
    }
}

点击收藏按钮并取消收藏:

主要思路是保证该Item可见的情况下,通过findViewByPosition找到目标Item的view,然后再找到处理监听事件的子View,click。

  private void clickFavourInStation(LinearLayoutManager mLinearLayoutManager, int i) {

        Log.d("TestTab", "点击收藏");

        ViewGroup targetGroup = (ViewGroup) mLinearLayoutManager.findViewByPosition(i);

        LineStnView mLineStnView = (LineStnView) targetGroup.getChildAt(1);

        if (mLineStnView.getX() < 0) {
            mLineStnView = (LineStnView) targetGroup.getChildAt(0);
        }

        View v = mLineStnView.findViewById(R.id.fav_view);

        solo.clickOnView(v);

        if (solo.waitForDialogToOpen()) {

            solo.clickOnText("取消收藏");

        } else {

            solo.clickOnView(v);

            solo.waitForDialogToOpen(2000);

            solo.clickOnText("取消收藏");
        }

    }

6、如果进入线路详情,遍历所有站点,刷新,然后换向后,重复一遍换向前操作。

主要思路是:通过getView得到list组件,通过组件给定的方法使我们想要选中的目标Item保持在屏幕内,然后findViewByPosition找到目标Item的组件,依次点击。

这里设置一个Flag值用来记录是否已经变向。

  private void traversalAllStat(Boolean flag) {

    solo.waitForEmptyActivityStack(2000);

    RealTimePanelContent content = (RealTimePanelContent) solo.getView(R.id.cll_real_time_panel_content);
    Log.d("TestTab", "获取到RealTimePanelContent的实例");

    LinearLayoutManager mLinearLayoutManager = (LinearLayoutManager) content.getLayoutManager();
    Log.d("TestTab", "获取到LinearLayoutManager的实例");

    int sum = content.getAdapter().getItemCount();//获取站数
    Log.d("TestTab", "获取到车站数" + String.valueOf(sum - 1));

    content.moveToPosition(3);
    solo.waitForEmptyActivityStack(2000);
    Log.d("TestTab", "等待2s到tab移动到左侧");


    int lastPosition = mLinearLayoutManager.findLastVisibleItemPosition();
    Log.d("TestTab", "界面出现的最后一个Item的position" + lastPosition);

    for (int i = 0; i < sum - 1; i++) {

        final ViewGroup viewGroup;
        Log.d("TestTab", "当前position为 " + String.valueOf(i));

        if (i < lastPosition) {

            solo.waitForEmptyActivityStack(1000);

            viewGroup = (ViewGroup) mLinearLayoutManager.findViewByPosition(i);

            if ((!solo.waitForCondition(new myCondition(viewGroup), 3000)) || (!viewGroup.isShown())) {
                Log.d("TestTab", "viewGroup为null");

                content.moveToPosition(i);
                Log.d("TestTab", "由于viewGroup为null,将" + i + "移动到中间");

                i--;
                Log.d("TestTab", "保持position位置不变");

                continue;
            }
            Log.d("TestTab", "viewGroup不为null");

            View viewToClick = viewGroup.findViewById(R.id.cll_apt_station_name);

            if ((!viewToClick.isShown()) || (!solo.waitForView(viewToClick, 1500, true))) {
            //这里反复验证取得View是否为空或者不可见,或者事件未触发
            }

                Log.d("TestTab", "viewToClick不可见");

                content.moveToPosition(i);
                Log.d("TestTab", "viewToClick不可见,将" + i + "移动到中间");

                i--;
                Log.d("TestTab", "保持position位置不变");

                continue;
            }

            solo.clickOnView(viewToClick);
            Log.d("TestTab", "点击到position" + String.valueOf(i));

            if (!solo.waitForDialogToClose(6000)) {

                Log.d("TestTab", "刷新框没有出现");

                content.moveToPosition(i);
                i--;
                Log.d("TestTab", "保持position位置不变");

                continue;
            }

            Log.d("TestTab", "点击收藏");
            clickFavourInLine();

            Log.d("TestTab", "点击刷新");
            clickRefresh();


        } else {

            content.moveToPosition(lastPosition);
            Log.d("TestTab", "将" + lastPosition + "移动到中间");

            solo.waitForEmptyActivityStack(2000);
            Log.d("TestTab", "等待2s");

            lastPosition = mLinearLayoutManager.findLastVisibleItemPosition();
            Log.d("TestTab", "改变lastPosition为" + lastPosition);

            viewGroup = (ViewGroup) mLinearLayoutManager.findViewByPosition(i);

            if ((!solo.waitForCondition(new myCondition(viewGroup), 3000)) || (!viewGroup.isShown())) {

                Log.d("TestTab", "viewGroup为null");

                content.moveToPosition(i);
                Log.d("TestTab", "将" + i + "移动到中间");

                i--;
                Log.d("TestTab", "保持position位置不变");

                continue;
            }

            View viewToClick = viewGroup.findViewById(R.id.cll_apt_station_name);

            if (!viewToClick.isShown()) {
                Log.d("TestTab", "viewToClick不可见");

                content.moveToPosition(i);
                Log.d("TestTab", "viewToClick不可见,将" + i + "移动到中间");

                i--;
                Log.d("TestTab", "保持position位置不变");

                continue;
            }

            solo.clickOnView(viewToClick);
            Log.d("TestTab", "点击到position" + String.valueOf(i));

            if (!solo.waitForDialogToOpen(6000)) {

                Log.d("TestTab", "刷新框没有出现");

                content.moveToPosition(i);
                i--;
                Log.d("TestTab", "保持position位置不变");

                continue;
            }

            Log.d("TestTab", "点击收藏");
            clickFavourInLine();

            Log.d("TestTab", "点击刷新");
            clickRefresh();
        }

        if (i == sum - 2 && flag) {

            flag = false;
            Log.d("TestTab", "切换标志,不可再换向");

            clickChangeDec();
            Log.d("TestTab", "点击换向");

            Log.d("TestTab", "重新遍历");
            traversalAllStat(flag);
        }

        if (!solo.waitForDialogToClose(60000)) {

            Log.d("TestTab", "刷新失败");
            solo.goBack();
        }
    }
}

五、Caution

1、最稳定最常用的的solo.clickOnView(solo.getView(R.id.xxx));可封装成根据ID进行点击,优先使用。

2、时间控制

速度过慢不符合我们快速测试的需求

速度过快,会导致两个问题,一是前一个动作未响应完(动作本身耗时长或者网络环境不好),后一个动作触发找不到组件;二是并行冲突,比如listView前一动作要求listView向下滑,后一个动作要求向上滑。

解决方法:waitXX方法等待动作完成,如果在规定时间内没有完成

3、界面的作用范围

作者:qq_22939165 发表于2015/8/19 15:18:27 原文链接
阅读:93 评论:0 查看评论

你可能感兴趣的:(测试,自动化,robotium)