在上一篇博客笔者介绍了Espresso的基础用法,在文章最后抛出了一个问题,简短的说就是异步的情况下,如何保证测试的正确进行。
如果没有看过的,建议先看这一篇,传送门在这里:
那么开始这篇博客的正题了
- 明确问题
- 解决方案
- 优雅的实现方式
- 实例演示
明确问题
在很多时候,我们都会进行网络请求,当进行网络请求的时候,由于网络的原因,我们不确定它什么时候可以返回给我结果。如果还是直接用上节的测试方法,很大概率会出现问题,因为测试代码是无脑顺序执行的,不知道什么时候它该停下来等待网络请求。
你或许会想到一个骚操作:在测试的时候,我在请求网络的时候让它睡个几秒(几秒你还不请求完成?),然后在继续执行测试代码。哈哈,这波操作还是很骚的,但是会遇到一个问题:你还是不确定这个等待时间是多少;如果睡时间短了,还是会测试错误,如果睡时间长了,就会浪费等待时间。所以,这个骚操作还是不可取的......有风险啊
那么该怎么办???选择狗带?
解决方案
既然Espresso是Google爸爸推崇的UI自动化测试工具,这个问题肯定有解决方法的。从上面的问题我们可以知道问题的根本原因就是我们不知道它什么时候完成网络请求。准确的说是异步操作的完成。
在这个基础上,Google给我们提供了IdlingResource这样一个接口
,通过这个接口,在我们测试的Activity中实现这个接口,通过里面的回调方法在通知测试类,我的异步操作完成了,你可以继续你的下一步测试了。
public interface IdlingResource {
/**
* 用来标识 IdlingResource 名称
*/
public String getName();
/**
* 当前 IdlingResource 是否空闲 .
*/
public boolean isIdleNow();
/**
注册一个空闲状态变换的ResourceCallback回调
*/
public void registerIdleTransitionCallback(ResourceCallback callback);
/**
* 通知Espresso当前IdlingResource状态变换为空闲的回调接口
*/
public interface ResourceCallback {
/**
* 当前状态转变为空闲时,调用该方法告诉Espresso
*/
public void onTransitionToIdle();
}
}
哇,看似好牛逼啊,但是这样的话我需要测试的每个Activity都要实现这个接口,还要实现这么多方法,多繁琐啊。会出现好多冗余的代码。在Activity添加代码是肯定要的了,但是我们可以减少冗余的代码量。以一个优雅的方式去实现。
优雅的实现方式
在使用IdlingResource之前,我们要添加两个库
implementation 'com.android.support.test.espresso:espresso-idling-resource:3.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-contrib:3.0.2'
注意第一个库要用implementation而不是androidTestImplementation,因为我们要在测试代码的外面使用IdlingResource,使用androidTestImplementation会找不到这个类,编译就不能通过。
接下来我们创建一个类实现IdlingResource接口
public class SimpleCountingIdlingResource implements IdlingResource {
private final String mResourceName;
//这个counter值就像一个标记,默认为0
private final AtomicInteger counter = new AtomicInteger(0);
private volatile ResourceCallback resourceCallback;
public SimpleCountingIdlingResource(String resourceNme){
mResourceName=resourceNme;
}
@Override
public String getName() {
return mResourceName;
}
@Override
public boolean isIdleNow() {
return counter.get()==0;
}
@Override
public void registerIdleTransitionCallback(ResourceCallback callback) {
resourceCallback=callback;
}
//每当我们开始异步请求,把counter值+1
public void increment(){
counter.getAndIncrement();
}
//当我们获取到网络数据后,counter值-1
public void decrement(){
int counterVal=counter.decrementAndGet();
//如果counterVal的值为0说明异步结束,执行回调
if(counterVal==0){
if(resourceCallback!=null){
resourceCallback.onTransitionToIdle();
}
}
if(counterVal<0)
//如果小于0,抛出异常
throw new IllegalArgumentException("Counter has been corrupted!");
}
}
这个类定义了一个标记counter,通过这个标记的值,来判断何时接口回调,从而测试类可以知道这个时候它的异步任务完成了,这时候就可以继续进行下一步的测试。
但是SimpleCountingIdlingResource这个类看起来还是有点杂乱的,我们再用一个管理类来封装它,让它处理业务部分。
ublic class EspressoIdlingResource {
private static final String RESOURCE = "GLOBAL";
private static SimpleCountingIdlingResource mCountingIdlingResource =
new SimpleCountingIdlingResource(RESOURCE);
public static void increment(){
mCountingIdlingResource.increment();
}
public static void decrement(){
mCountingIdlingResource.decrement();
}
public static IdlingResource getIdlingResource(){
return mCountingIdlingResource;
}
}
所以最终我们只需要直接使用EspressoIdlingResource这个类就行了。
说这么多还是太抽象了,下面用一个实例来感受一下。
实例演示
还是用之前的登录来进行测试,不过添加了一个线程睡眠来模拟一个网络请求的等待时间。
MainActivity.class
public class MainActivity extends AppCompatActivity {
EditText edName;
EditText edPass;
Button btClick;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btClick=(Button)findViewById(R.id.bt_click);
edName=(EditText) findViewById(R.id.ed_username);
edPass=(EditText) findViewById(R.id.ed_pass);
btClick.setText("登录");
}
public void clickButton(View view){
btClick.setText("登录中...");
MyThread myThread=new MyThread();
myThread.start();
}
class MyThread extends Thread{
@Override
public void run() {
super.run();
try {
Thread.sleep(5000); //让该线程睡眠5秒
} catch (InterruptedException e) {
e.printStackTrace();
}
if(edName.getText().toString().equals("jasonking")&&edPass.getText().toString().equals("123")){
runOnUiThread(new Runnable() {
@Override
public void run() {
btClick.setText("登录成功");
}
});
}else{
runOnUiThread(new Runnable() {
@Override
public void run() {
btClick.setText("登录失败");
}
});
}
}
}
}
如果我们继续用之前的测试用例,运行测试会发现,测试不能通过。因为我们期盼的是“登录成功”,但是5s内,我们得到的结果是“登录中...”,只有5秒之后才可能返回"登录成功。
接下来,我们就可以使用之前准备的工具类了,对这个Activity进行标记
异步开始前的标记
public void clickButton(View view){
btClick.setText("登录中...");
//在开始异步请求前添加这行代码,意味着开始了异步
EspressoIdlingResource.increment();
MyThread myThread=new MyThread();
myThread.start();
}
异步结束后的标记
class MyThread extends Thread{
@Override
public void run() {
//省略...
//异步结束的时候,添加这行代码
if (!EspressoIdlingResource.getIdlingResource().isIdleNow()) {
EspressoIdlingResource.decrement();
}
}
}
添加这个方法,获取这个类的标识
@VisibleForTesting
public IdlingResource getCountingIdlingResource() {
return EspressoIdlingResource.getIdlingResource();
}
最后再修改一下测试类
MyEspressoAsyncTest.class
相比较之前的,这里多做了3个步骤
- 获取需要测试的Activity的标识,之后为了添加到异步监听集合中
- 注册异步监听
- 在测试结束后取消注册,释放资源
@RunWith(AndroidJUnit4.class)
@LargeTest
public class MyEspressoAsyncTest {
@Rule
public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<>(MainActivity.class);
private IdlingResource idlingResource;
@Before
public void setUp() throws Exception{
//获取这个类的标识
idlingResource=mActivityRule.getActivity().getCountingIdlingResource();
}
@Test
public void onLoadingFinished(){
//清空文本框,然后输入用户名jasonking,关闭软键盘
onView(withId(R.id.ed_username))
.perform(
clearText(),
replaceText("jasonking"),
closeSoftKeyboard()
)
.check(matches(withText("jasonking")));
//清空文本框,然后输入密码,关闭软键盘
onView(withId(R.id.ed_pass))
.perform(
clearText(),
replaceText("123"),
closeSoftKeyboard()
)
.check(matches(withText("123")));
//点击按钮检查文本是不是登录
onView(withId(R.id.bt_click))
.check(matches(withText("登录")))
.perform(click());
//注册异步监听,,此时测试会挂起,等待网络请求结束后,继续测试
IdlingRegistry.getInstance().register(idlingResource);
Log.d(TAG, "setUp: "+"请求网络请求完成");
//继续执行代码
onView(withId(R.id.bt_click))
.check(matches(withText("登录成功")));
}
@After
public void release() throws Exception {
// 当然,我们需要在测试结束后取消注册,释放资源
IdlingRegistry.getInstance().unregister(idlingResource);
}
}
运行测试可以看到结果是pass的