关闭

Android单元测试框架Robolectric3.0介绍(2)

发表于:2016-6-08 13:44

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:geniusmart    来源:51Testing软件测试网采编

  3. 网络请求的异步回调如何进行测试
  关于网络请求之后的回调函数如何测试,笔者暂时也没有什么自己觉得满意的解决方案,这里提供一种做法,权当抛砖引玉,希望有此经验的人提供更多的思路。
  由于网络请求和回调函数是在子线程和UI主线程两个线程中进行的,且后者要等待前者执行完毕,这种情况要在一个TestCase中测试并不容易。因此我们要做的就是想办法让两件事情同步的在一个TestCase中执行,类似于这样的代码:
  //此为Retrofit2的新api,代表同步执行
  //异步执行的api为githubService.followingUser("geniusmart").enqueue(callback);
  githubService.publicRepositories("geniusmart").execute();
  callback.onResponse(call,response);
  //对执行回调后影响的数据做断言
  some assert...
  这里我列举一个场景,并进行相应的单元测试:一个Activity中有个ListView,经过网络请求后,在异步回调函数里加载ListView的数据,点击每一个item后,吐司其对应的标题。
public class CallbackActivity extends Activity {
//省略一些全局变量声明的代码
/**
* 定义一个全局的callback对象,并暴露出get方法供UT调用
*/
private Callback<List<User>> callback;
@Override
protected void onCreate(Bundle savedInstanceState) {
//省略一些初始化UI组件的代码
listView.setOnItemClickListener(new AdapterView.OnItemClickListener(){
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Toast.makeText(CallbackActivity.this,datas.get(position),Toast.LENGTH_SHORT).show();
}
});
//加载数据
loadData();
}
public void loadData() {
progressBar.setVisibility(View.VISIBLE);
datas = new ArrayList<>();
//初始化回调函数对象
callback = new Callback<List<User>>() {
@Override
public void onResponse(Call<List<User>> call, Response<List<User>> response) {
for(User user : response.body()){
datas.add(user.login);
}
ArrayAdapter<String> adapter = new ArrayAdapter<>(CallbackActivity.this,
android.R.layout.simple_list_item_1, datas);
listView.setAdapter(adapter);
progressBar.setVisibility(View.GONE);
}
@Override
public void onFailure(Call<List<User>> call, Throwable t) {
progressBar.setVisibility(View.GONE);
}
};
GithubService githubService = GithubService.Factory.create();
githubService.followingUser("geniusmart").enqueue(callback);
}
public Callback<List<User>> getCallback(){
return callback;
}
}
  相应的测试代码如下:
@Test
public void callback() throws IOException {
CallbackActivity callbackActivity = Robolectric.setupActivity(CallbackActivity.class);
ListView listView = (ListView) callbackActivity.findViewById(R.id.listView);
Response<List<User>> users = mockGithubService.followingUser("geniusmart").execute();
//结合模拟的响应数据,执行回调函数
callbackActivity.getCallback().onResponse(null, users);
ListAdapter listAdapter = listView.getAdapter();
//对ListView的item进行断言
assertEquals(listAdapter.getItem(0).toString(), "JakeWharton");
assertEquals(listAdapter.getItem(1).toString(), "Trinea");
ShadowListView shadowListView = Shadows.shadowOf(listView);
//测试点击ListView的第3~5个Item后,吐司的文本
shadowListView.performItemClick(2);
assertEquals(ShadowToast.getTextOfLatestToast(), "daimajia");
shadowListView.performItemClick(3);
assertEquals(ShadowToast.getTextOfLatestToast(), "liaohuqiu");
shadowListView.performItemClick(4);
assertEquals(ShadowToast.getTextOfLatestToast(), "stormzhang");
}
  这样做的话要改变一些编码习惯,比如回调函数不能写成匿名内部类对象,需要定义一个全局变量,并破坏其封装性,即提供一个get方法,供UT调用。
  四、数据库篇
  Robolectric从2.2开始,就已经可以对真正的DB进行测试,从3.0开始测试DB变得更加便利,通过UT来调试DB简直不能更爽。这一节将介绍不使用任何框架的DB测试,ORMLite测试以及ContentProvider测试。
  1. 不使用任何框架的DB测试(SQLiteOpenHelper)
  如果没有使用框架,采用Android的SQLiteOpenHelper对数据库进行操作,通常我们会封装好各个Dao,并实例化一个SQLiteOpenHelper的单例对象,测试代码如下:
  @Test
  public void query(){
  AccountDao.save(AccountUtil.createAccount("3"));
  AccountDao.save(AccountUtil.createAccount("4"));
  AccountDao.save(AccountUtil.createAccount("5"));
  AccountDao.save(AccountUtil.createAccount("5"));
  List<Account> accountList = AccountDao.query();
  assertEquals(accountList.size(), 3);
  }
  另外有一点要注意的是,当我们测试多个test时,会抛出一个类似于这样的异常:
  java.lang.RuntimeException: java.lang.IllegalStateException: Illegal connection pointer 37. Current pointers for thread Thread[pool-1-thread-1,5,main] []
  解决方式便是每次执行一个test之后,就将SQLiteOpenHelper实例对象重置为null,如下:
  @After
  public void tearDown(){
  AccountUtil.resetSingleton(AccountDBHelper.class, "mAccountDBHelper");
  }
  public static void resetSingleton(Class clazz, String fieldName) {
  Field instance;
  try {
  instance = clazz.getDeclaredField(fieldName);
  instance.setAccessible(true);
  instance.set(null, null);
  } catch (Exception e) {
  throw new RuntimeException();
  }
  }
  2. OrmLite测试
  使用OrmLite对数据操作的测试与上述方法并无区别,同样也要注意每次测试完后,要重置OrmLiteSqliteOpenHelper实例。
  @After
  public void tearDown(){
  DatabaseHelper.releaseHelper();
  }
  @Test
  public void save() throws SQLException {
  long millis = System.currentTimeMillis();
  dao.create(new SimpleData(millis));
  dao.create(new SimpleData(millis + 1));
  dao.create(new SimpleData(millis + 2));
  assertEquals(dao.countOf(), 3);
  List<SimpleData> simpleDatas = dao.queryForAll();
  assertEquals(simpleDatas.get(0).millis, millis);
  assertEquals(simpleDatas.get(1).string, ((millis + 1) % 1000) + "ms");
  assertEquals(simpleDatas.get(2).millis, millis + 2);
  }
  3. ContentProvider测试
  一旦你的App里有ContentProvider,此时配备完善和严谨的单元测试用例是非常有必要的,毕竟你的ContentProvider是对外提供使用的,一定要保证代码的质量和稳定性。
  对ContentProvider的测试,需要借助影子对象ShadowContentResolver,关于Shadow,我在上文中已经有介绍过,此处的Shadow可以丰富ContentResolver的行为,帮助我们进行测试,代码如下:
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class)
public class AccountProviderTest {
private ContentResolver mContentResolver;
private ShadowContentResolver mShadowContentResolver;
private AccountProvider mProvider;
private String AUTHORITY = "com.geniusmart.loveut.AccountProvider";
private Uri URI_PERSONAL_INFO = Uri.parse("content://" + AUTHORITY + "/" + AccountTable.TABLE_NAME);
@Before
public void setUp() {
ShadowLog.stream = System.out;
mProvider = new AccountProvider();
mContentResolver = RuntimeEnvironment.application.getContentResolver();
//创建ContentResolver的Shadow对象
mShadowContentResolver = Shadows.shadowOf(mContentResolver);
mProvider.onCreate();
//注册ContentProvider对象和对应的AUTHORITY
ShadowContentResolver.registerProvider(AUTHORITY, mProvider);
}
@After
public void tearDown() {
AccountUtil.resetSingleton(AccountDBHelper.class, "mAccountDBHelper");
}
@Test
public void query() {
ContentValues contentValues1 = AccountUtil.getContentValues("1");
ContentValues contentValues2 = AccountUtil.getContentValues("2");
mShadowContentResolver.insert(URI_PERSONAL_INFO, contentValues1);
mShadowContentResolver.insert(URI_PERSONAL_INFO, contentValues2);
//查询所有数据
Cursor cursor1 = mShadowContentResolver.query(URI_PERSONAL_INFO, null, null, null, null);
assertEquals(cursor1.getCount(), 2);
//查询id为2的数据
Uri uri = ContentUris.withAppendedId(URI_PERSONAL_INFO, 2);
Cursor cursor2 = mShadowContentResolver.query(uri, null, null, null, null);
assertEquals(cursor2.getCount(), 1);
}
@Test
public void queryNoMatch() {
Uri noMathchUri = Uri.parse("content://com.geniusmart.loveut.AccountProvider/tabel/");
Cursor cursor = mShadowContentResolver.query(noMathchUri, null, null, null, null);
assertNull(cursor);
}
@Test
public void insert() {
ContentValues contentValues1 = AccountUtil.getContentValues("1");
mShadowContentResolver.insert(URI_PERSONAL_INFO, contentValues1);
Cursor cursor = mShadowContentResolver.query(URI_PERSONAL_INFO, null, AccountTable.ACCOUNT_ID + "=?", new String[]{"1"}, null);
assertEquals(cursor.getCount(), 1);
cursor.close();
}
@Test
public void update() {
ContentValues contentValues = AccountUtil.getContentValues("2");
Uri uri = mShadowContentResolver.insert(URI_PERSONAL_INFO, contentValues);
contentValues.put(AccountTable.ACCOUNT_NAME, "geniusmart_update");
int update = mShadowContentResolver.update(uri, contentValues, null, null);
assertEquals(update, 1);
Cursor cursor = mShadowContentResolver.query(URI_PERSONAL_INFO, null, AccountTable.ACCOUNT_ID + "=?", new String[]{"2"}, null);
cursor.moveToFirst();
String accountName = cursor.getString(cursor.getColumnIndex(AccountTable.ACCOUNT_NAME));
assertEquals(accountName, "geniusmart_update");
cursor.close();
}
@Test
public void delete() {
try {
mShadowContentResolver.delete(URI_PERSONAL_INFO, null, null);
fail("Exception not thrown");
} catch (Exception e) {
assertEquals(e.getMessage(), "Delete not supported");
}
}
}
  五、Love UT
  写UT是一种非常好的编程习惯,但是UT虽好,切忌贪杯,作为一名技术领导者,切忌拿测试覆盖率作为指标,如此一来会滋生开发者的抵触心理,导致乱写一通。作为开发者,应该时刻思考什么才是有价值的UT,什么逻辑没必要写(比如set和get),这样才不会疲于奔命且觉得乏味。其实很多事情都是因果关系,开发人员不写,所以leader强制写,而leader强制写,开发人员会抵触而乱写。所以,让各自做好,一起来享受UT带来的高质量的代码以及为了可测试而去思考代码设计的编程乐趣。
  本文的所有代码仍然放在LoveUT这个工程里:
  https://github.com/geniusmart/LoveUT
相关文章:
Android单元测试框架Robolectric3.0介绍(1)
22/2<12
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号