Robotium 自动化测试

标签: robotium 自动化 测试 | 发表时间:2015-08-19 23:18 | 作者:qq_22939165
出处:http://blog.csdn.net

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 自动化 测试] 推荐:

Android Robotium自动化测试

- - CSDN博客移动开发推荐文章
1、官方网站下载测试工程demo. 从 http://code.google.com/p/robotium/downloads/detail?name=ExampleTestProject_v3.6.zip 下载官方的Android测试工程demo. 解压后的文件NotePad、NotePadTest、readme.txt.

Robotium 自动化测试

- - CSDN博客推荐文章
Robotium 自动化测试. Android Studio环境下,在所要测试的Module的build.gradle文件下添加,. Robotium即是对Instrumentation框架方法的封装,所以使用之前需要继承测试类,重写构造器,setUp()和tearDown()方法. 其中继承的是ActivityInstrumentationTestCase2测试类.

iPhone App自动化测试

- BeerBubble - Taobao QA Team
         无线客户端的发展很快,特别针对是android和ios两款无线操作系统的客户端应用,相应的测试工具也应运而生,这里主要给大家介绍一些针对iPhone App的自动化测试工具.          首先,我们把这些测试框架分为三大类:接口测试工具、注入式UI测试工具、录放式UI测试工具.

Android UiAutomator 自动化测试

- - 操作系统 - ITeye博客
一、一个BUG引发的问题.     如果研发过程中有一个BUG:“不断的切换手机语言出现花屏现象”. 我想,最好的方式应该是自动化测试.     那么,自动化测试可以完成哪些任务呢.     简单的说,那些重复性的测试工作,都可以交给自动化完成:.         1、设置手机的语言.         2、添加、删除、收藏联系人.

Android自动化测试解决方案

- Haides - InfoQ中文站
现在,已经有大量的Android自动化测试架构或工具可供我们使用,其中包括:Activity Instrumentation, MonkeyRunner,Robotium,以及 Robolectric. 另外LessPainful也提供服务来进行真实设备上的自动化测试.

InstrumentDriver,对iOS自动化测试说 Yes!

- - Taobao QA Team
    InstrumentDriver 是 Mobile自动化小组最近实现的基于 instrument,针对 iOS 的自动化测试框架,目前支持 java 语言编写测试用例.     研究过iOS自动化测试的同学肯定对 instrument UI Automation 有所耳闻,或者已经使用它进行自动化测试实践.

菜鸟学自动化测试(九)----WebDirver

- - 博客园_首页
关于什么是WebDirver,上一节做了简单的描述,环境也在上一章中搭建完成. 下面我们拷贝了官网提供的一个实例. 让其在我们的eclipse中运行. Selenium WebDirver 代码如下:. // 用Firefox driver创建一个新的的实例. //注意:其他的代码依赖于界面. WebDriver driver = new FirefoxDriver();// 这里我们可以使用firefox来运行测试用例.

无用的自动化测试

- - CSDN博客研发管理推荐文章
自动化测试,特别是UI级的自动化测试是一件费力而不讨好的事情. 自动化测试使得测试人员疲于应付,朝不顾夕,如坐针毡,苟延残喘. UI级的自动化测试看起来很美好,就像罂粟,如果你经不住诱惑冒然尝试,那么后果很严重,下场很惨淡. 也许这个世界上就不应该出现自动化测试这个东西,起码在中国不应该出现,因为这个是无效的,无用的,宿命是失败的东西.

前端自动化测试探索

- - FEX 百度 Web 前端研发部
测试是完善的研发体系中不可或缺的一环. 前端同样需要测试,你的css改动可能导致页面错位、js改动可能导致功能不正常. 由于前端偏向GUI软件的特殊性,尽管测试领域工具层出不穷,在前端的自动化测试上面却实施并不广泛,很多人依旧以手工测试为主. 本文试图探讨前端自动化测试领域的工具和实践. 一个项目最终会经过快速迭代走向以维护为主的状态,在合理的时机以合理的方式引入自动化测试能有效减少人工维护成本.

Android 自动化测试工具初探

- - IT瘾-geek
Android 自动化测试工具初探.    这段几乎都编写代码,没有新的心得体会.唯一由感想的是在测试上.由于策划的变动,接口的完善等因素,总在不停的修改功能,修改代码.由于项目中的代码都经过了好多少,又没有很好的架构规划.所以在修改或测试的时候难免会有遗漏的地方,这个时候就在想android是不是也应该有自动化测试工具来辅助测试.使得功能更完善点.本期的创新文档只能算是对自动化创新工具的一种简介..