Espresso中IdlingResource详解

在ui测试中经常会遇到异步线程的操作(例如网络请求,后台计算等操作后验证界面元素),在uiautomator中一般会使用sleep方法来进行等待,在espresso中已经不推荐使用sleep这种形式来等待异步线程操作完成了。

IdlingResource

在espresso中,基于AsynTask的异步任务都会自动在测试中转化为同步线程,例如基于rxjava,rxandroid的异步网络请求,在等网络请求返回结果后Espresso才会执行下一步的操作,除了AsynTask外还有其他的异步操作,例如IntentService等,在这些场景下就需要用到IdlingResource了.

先贴一下idlingresource源码:

public interface IdlingResource
    {
    // 返回idlingresource名字,必须返回
    public String getName();
    // 主线程执行的方法,主要实现该方法
    public boolean isIdleNow();
    public void registerIdleTransitionCallback(ResourceCallback callback);
    public interface ResourceCallback {
    //回调方法,用于通知主线程是否从busy-idle
         public void onTransitionToIdle();
    }
}

要使IdlingResource生效,必须在测试时先进行注册,通过espresso.registerIdlingResources(yourIdlingResource);注册成功后,IdlingResource在运行时就会检查主线程的运行状态,espresso的IdlingResourceRegistry实现了onTransitionToIdle()的回调方法

private void registerToIdleCallback(final IdlingResource resource, final int position) {
     resource.registerIdleTransitionCallback(new ResourceCallback() {
         @Override
         public void onTransitionToIdle() {
              Message m = handler.obtainMessage(DYNAMIC_RESOURCE_HAS_IDLED);
              m.arg1 = position;
              m.obj = resource;
              handler.sendMessage(m);
         }
     });
}

IdlingResource在主线程中通过isIdleNow方法来判断主线程是否idle,若idle则回调onTransitionToIdle()方法告知espresso我已经处于idle状态,若false继续通过isIdleNow状态检测,直到超时或者回调onTransitionToidle()方法为止

附上自己写的一个小例子,场景是发送图片动态,界面出现发送成功后通知espresso执行下一步操作,其中textview由外部传入,通过activityTestRule的getActivity()方法获取当前activity并通过findViewById获取:

public class SendingIdlingResources implements IdlingResource{
     private TextView textView;
     private ResourceCallback mResourceCallback;
     public SendingIdlingResources(TextView textView){ this.textView=textView; }
     @Override
     public String getName() { return "sendingidlingresources"; }
     @Override public boolean isIdleNow() {
          String text = (String) textView.getText();
          String expectedString = "发送成功";
          boolean idle = (text.equals(expectedString));
          if (idle){
               System.out.println("发送成功:"+text);
               mResourceCallback.onTransitionToIdle();
          } else{
                 System.out.println("发送成功没有出现:"+text);
                }
          return idle;
      }
      @Override public void registerIdleTransitionCallback(ResourceCallback callback) {
                      this.mResourceCallback = callback;
       }
}

Espresso中RecyclerView操作

RecyclerViewActions
espresso中如果我们要对AdapterView进行操作,我们会使用onData()方法,然而在对RecyclerView使用onData()方法时会报错,因为RecyclerView是继承自ViewGroup而非AdapterView,如何对RecyclerView进行操作,google官方提供了对应的支持:

androidTestCompile ('com.android.support.test.espresso:espresso-contrib:2.2')

代码如下,点击recyclerview中的第五个item:

onView(withId(R.id.swipe_target)).perform(RecyclerViewActions.actionOnItemAtPosition(4,click()));

官方仅仅只对recyclerview提供了操作的api,如何check RecyclerView中的元素并没有提供。

RecyclerViewMatcher
官方没有提供,但是官方给出了建议,由自己实现自定义viewMathcer来匹配定位元素,github已有大神实现:
https://github.com/dannyroa/espresso-samples/tree/master/RecyclerView

public static RecyclerViewMatcher withRecyclerView(final int recyclerViewId) {
    return new RecyclerViewMatcher(recyclerViewId);
}
//检查recyclerview中第4个item是否含有内容为“hello”
onView(withRecyclerView(R.id.scroll_view).atPosition(3))
    .check(matches(hasDescendant(withText("hello"))));
// 点击该item
onView(withRecyclerView(R.id.scroll_view).atPosition(3)).perform(click());

对于屏幕外的元素
以上方法在对屏幕外的item操作时会报错,如何解决:

onView(withId(recyclerViewId)).perform(scrollToPosition(position));
onView(withRecyclerView(recyclerViewId).atPosition(position)).perform(click());

通过scroll方法首先滚动到屏幕外的元素,再对元素进行check或者操作即可。

sqlite增删改查的两种方式

/**
 * Created by cc05451 on 17/1/9.
 * sqlite增删改查的两种方式
 */


public class InfoDao {
    private MySqliteOpenHelper mySqliteOpenHelper;
    public InfoDao(Context context)
    {
        mySqliteOpenHelper = new MySqliteOpenHelper(context);
    }

    /***
     * 写入数据库方法一
     * @param bean
     */

    public void add(InfoBean bean){
        SQLiteDatabase db = mySqliteOpenHelper.getReadableDatabase();
        String sql = "insert into info(name,phone) values(?,?);";
        /***
         * 第一个参数sql:sql语句
         * 第二个参数bindArgs:sql语句中的占位符(?)
         */

        db.execSQL(sql,new Object[]{bean.name,bean.phone});
        db.close();
    }

    /***
     * 写入数据库方法二
     * @param bean
     * @return boolean
     */

    public boolean add2(InfoBean bean){
        SQLiteDatabase db = mySqliteOpenHelper.getReadableDatabase();
        ContentValues values = new ContentValues();
        values.put("name",bean.name);
        values.put("phone",bean.phone);
        /***
         * 第一个参数table:表名
         * 第二个参数nullcolumnhack:当contentvalues为空时插入的数据
         * 第三个参数:插入数据库的值
         * 返回-1表示出错,成功返回添加新行的id
         */

        long result = db.insert("info",null,values);
        db.close();
        if (result!=-1){
            return true;
        }else{
            return false;
        }
    }

    /***
     * 删除数据方法一
     * @param name
     */

    public void del(String name){
        SQLiteDatabase db = mySqliteOpenHelper.getReadableDatabase();
        String sql = "delete from info where name=?;";
        /***
         * 第一个参数sql:sql语句
         * 第二个参数bindArgs:sql语句中的占位符(?)
         */

        db.execSQL(sql,new Object[]{name});
        db.close();

    }

    /***
     * 删除数据方法二
     * @param name
     * @return
     */

    public int del2(String name)
    {
        SQLiteDatabase db = mySqliteOpenHelper.getReadableDatabase();
        /***
         * 第一个参数table:表名
         * 第二个参数删除条件
         * 第三个参数sql语句占位符
         * 返回删除的行数
         */

        int result = db.delete("info","name = ?",new String[]{name});
        db.close();
        return result;
    }

    /***
     * 更新数据方法一
     * @param bean
     */

    public void update(InfoBean bean){
        SQLiteDatabase db = mySqliteOpenHelper.getReadableDatabase();
        String sql = "update info set phone=? where name=?;";
        /***
         * 第一个参数sql:sql语句
         * 第二个参数bindArgs:sql语句中的占位符(?)
         */

        db.execSQL(sql,new Object[]{bean.phone,bean.name});
        db.close();

    }
    /***
     * 更新数据方法二
     * @param bean
     * @return
     */

    public int update2(InfoBean bean){
        SQLiteDatabase db = mySqliteOpenHelper.getReadableDatabase();
        ContentValues values = new ContentValues();
        values.put("phone",bean.phone);
        /***
         * 第一个参数table:表名
         * 第二个参数更新条件
         * 第三个参数sql语句占位符
         * 返回更新的行数
         */

        int update = db.update("info",values,"name = ?",new String[]{bean.name});
        db.close();
        return update;
    }

    /***
     * 查询数据方法一
     * @param name
     */

    public void select(String name){
        System.out.println("selectxxxx");
        SQLiteDatabase db = mySqliteOpenHelper.getReadableDatabase();
        String sql = "select id,name,phone from info where name=?;";
        Cursor cursor = db.rawQuery(sql,new String[]{name});
        if(cursor!=null && cursor.getCount()>0) {
            while (cursor.moveToNext()) {
                int id = cursor.getInt(0);
                String name_str = cursor.getString(1);
                String phone = cursor.getString(2);
                System.out.println("_id:" + id + "name:" + name_str + "phone:" + phone);

            }
            cursor.close();
        }
        db.close();

    }

    /***
     * 查询数据方法二
     * @param name
     */


    public void select2(String name){
        SQLiteDatabase db = mySqliteOpenHelper.getReadableDatabase();
        /***
         * 第一个参数table:表名
         * 第二个参数columns:查询的列名,为空查询所有列
         * 第三个参数selection:查询条件
         * 第四个参数selectionArgs:查询条件占位符
         * 第五个参数groupby:分组
         * 第六个参数having:分组条件
         * 第七个参数orderby:排序
         */

        Cursor cursor = db.query("info",new String[]{"id","name","phone"},"name = ?",new String[]{name},null,null,"id desc");
        if(cursor!=null && cursor.getCount()>0) {
            while (cursor.moveToNext()) {
                int id = cursor.getInt(0);
                String name_str = cursor.getString(1);
                String phone = cursor.getString(2);
                System.out.println("_id:" + id + "name:" + name_str + "phone:" + phone);

            }
            cursor.close();
        }
        db.close();

    }

}

SharedPreferences使用

SharedPreferences 类提供了一个通用框架,以便您能够保存和检索原始数据类型的永久性键值对。 您可以使用 SharedPreferences 来保存任何原始数据:布尔值、浮点值、整型值、长整型和字符串。 此数据将跨多个用户会话永久保留(即使您的应用已终止亦如此)。

要获取应用的 SharedPreferences 对象,请使用以下两个方法之一:

  • getSharedPreferences() – 如果您需要多个按名称(使用第一个参数指定)识别的首选项文件,请使用此方法。
  • getPreferences() – 如果您只需要一个用于 Activity 的首选项文件,请使用此方法。 由于这将是用于 Activity 的唯一首选项文件,因此无需提供名称。

要写入值:

  1. 调用 edit() 以获取 SharedPreferences.Editor
  2. 使用 putBoolean() 和 putString() 等方法添加值。
  3. 使用 commit() 提交新值

要读取值,请使用 getBoolean() 和 getString() 等 SharedPreferences 方法。

——节选自官方文档

实际使用案例

存储SharedPreferences

public static boolean saveUserInfo(Context context, String username, String password) {
//      1.通过context对象创建sharedpreference对象
//        name:sharedpreference文件名 mode:文件操作模式
        try {
            SharedPreferences sharedPreference = context.getSharedPreferences("userinfo.txt", Context.MODE_PRIVATE);
//      2.通过sharedpreference对象获取一个editor对象
            SharedPreferences.Editor editor = sharedPreference.edit();
//      3.往editor中添加数据
            editor.putString("username", username);
            editor.putString("password", password);
            editor.commit();
            return true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

读取SharedPreferences

public static Map<String, String> getUserInfo(Context context) {
//        1.创建sharedpreference对象
        SharedPreferences sharedpreferences = context.getSharedPreferences("userinfo.txt", Context.MODE_PRIVATE);
//        2.通过sharedpreference获取存放数据
        String username = sharedpreferences.getString("username", "");
        String password = sharedpreferences.getString("password", "");
//
        HashMap<String, String> hashMap = new HashMap<String, String>();
        hashMap.put("username", username);
        hashMap.put("password", password);
        return hashMap;
    }