如何使用 Espresso Idling Resource 进行网络通话
How to use Espresso Idling Resource for network calls
我正在尝试使用 Espresso 来测试我的 UI。当我登录我的应用程序时,我调用 Parse API(网络调用)来验证用户名和密码。如果一切顺利,用户将被定向到一个新的 activity。我想对此进行测试,但我似乎无法使用空闲资源。
代码:
public class ApplicationTest extends ActivityInstrumentationTestCase2<LoginActivity> {
private CountingIdlingResource fooServerIdlingResource;
public ApplicationTest() {
super(LoginActivity.class);
}
@Before
public void setUp() throws Exception {
super.setUp();
injectInstrumentation(InstrumentationRegistry.getInstrumentation());
getActivity();
CountingIdlingResource countingResource = new CountingIdlingResource("FooServerCalls");
this.fooServerIdlingResource = countingResource;
Espresso.registerIdlingResources(countingResource);
}
public void testChangeText_sameActivity() {
// Type text and then press the button.
onView(withId(R.id.username))
.perform(typeText("s@s.nl"), closeSoftKeyboard());
onView(withId(R.id.password))
.perform(typeText("s"), closeSoftKeyboard());
if(performClick())
onView(withId(R.id.main_relative_layout))
.check(matches(isDisplayed()));
// Check that the text was changed.
}
public boolean performClick(){
fooServerIdlingResource.increment();
try {
onView(withId(R.id.login)).perform(click());
return true;
} finally {
fooServerIdlingResource.decrement();
}
}
@SuppressWarnings("javadoc")
public final class CountingIdlingResource implements IdlingResource {
private static final String TAG = "CountingIdlingResource";
private final String resourceName;
private final AtomicInteger counter = new AtomicInteger(0);
private final boolean debugCounting;
// written from main thread, read from any thread.
private volatile ResourceCallback resourceCallback;
// read/written from any thread - used for debugging messages.
private volatile long becameBusyAt = 0;
private volatile long becameIdleAt = 0;
/**
* Creates a CountingIdlingResource without debug tracing.
*
* @param resourceName the resource name this resource should report to Espresso.
*/
public CountingIdlingResource(String resourceName) {
this(resourceName, false);
}
/**
* Creates a CountingIdlingResource.
*
* @param resourceName the resource name this resource should report to Espresso.
* @param debugCounting if true increment & decrement calls will print trace information to logs.
*/
public CountingIdlingResource(String resourceName, boolean debugCounting) {
this.resourceName = checkNotNull(resourceName);
this.debugCounting = debugCounting;
}
@Override
public String getName() {
return resourceName;
}
@Override
public boolean isIdleNow() {
return counter.get() == 0;
}
@Override
public void registerIdleTransitionCallback(ResourceCallback resourceCallback) {
this.resourceCallback = resourceCallback;
}
/**
* Increments the count of in-flight transactions to the resource being monitored.
* <p/>
* This method can be called from any thread.
*/
public void increment() {
int counterVal = counter.getAndIncrement();
if (0 == counterVal) {
becameBusyAt = SystemClock.uptimeMillis();
}
if (debugCounting) {
Log.i(TAG, "Resource: " + resourceName + " in-use-count incremented to: " + (counterVal + 1));
}
}
/**
* Decrements the count of in-flight transactions to the resource being monitored.
* <p/>
* If this operation results in the counter falling below 0 - an exception is raised.
*
* @throws IllegalStateException if the counter is below 0.
*/
public void decrement() {
int counterVal = counter.decrementAndGet();
if (counterVal == 0) {
// we've gone from non-zero to zero. That means we're idle now! Tell espresso.
if (null != resourceCallback) {
resourceCallback.onTransitionToIdle();
}
becameIdleAt = SystemClock.uptimeMillis();
}
if (debugCounting) {
if (counterVal == 0) {
Log.i(TAG, "Resource: " + resourceName + " went idle! (Time spent not idle: " +
(becameIdleAt - becameBusyAt) + ")");
} else {
Log.i(TAG, "Resource: " + resourceName + " in-use-count decremented to: " + counterVal);
}
}
checkState(counterVal > -1, "Counter has been corrupted!");
}
/**
* Prints the current state of this resource to the logcat at info level.
*/
public void dumpStateToLogs() {
StringBuilder message = new StringBuilder("Resource: ")
.append(resourceName)
.append(" inflight transaction count: ")
.append(counter.get());
if (0 == becameBusyAt) {
Log.i(TAG, message.append(" and has never been busy!").toString());
} else {
message.append(" and was last busy at: ")
.append(becameBusyAt);
if (0 == becameIdleAt) {
Log.w(TAG, message.append(" AND NEVER WENT IDLE!").toString());
} else {
message.append(" and last went idle at: ")
.append(becameIdleAt);
Log.i(TAG, message.toString());
}
}
}
}
}
我现在得到的异常如下:
ndroid.support.test.espresso.IdlingResourceTimeoutException: Wait for [FooServerCalls] to become idle timed out
当我 运行 测试时,用户名和密码正在填写,但从未调用执行点击,几秒钟后我收到异常。如何正确实现空闲资源?
编辑 --
我建议 Android 使用 Calabash。 Calabash 的工作原理类似,但不需要您更改应用程序代码来进行测试。
Espresso 将在执行点击(或任何查看操作)之前轮询空闲资源。但是在点击 之后,您不会减少计数器。这是一个僵局。
我在这里看不到任何快速修复;你的方法对我来说真的没有意义。我想到了一些可能的替代方法:
- 根据您用于网络的库,您或许可以编写一个空闲资源来检查是否有正在进行的呼叫。
- 如果您在调用登录时显示进度指示器,则可以安装一个等待它消失的 IdlingResource。
- 您可以等待下一个 activity 启动,或者等待某个视图 appear/disappear。
正如其他答案所暗示的那样,countingIdlingResource 并不真正适用于您的用例。
我经常做的是添加一个接口 - 让我们称之为 ProgressListener
- 作为 activity / 片段的一个字段,它有一个要等待的资源(异步后台工作,更长网络会话等)以及每次显示或取消进度时通知它的方法。
我假设您有凭据验证逻辑和对 LoginActivity
中的 Parse API 的调用,如果成功,它将调用对 MainActivity
的意图。
public class LoginActivity extends AppCompatActivity {
private ProgressListener mListener;
...
public interface ProgressListener {
public void onProgressShown();
public void onProgressDismissed();
}
public void setProgressListener(ProgressListener progressListener) {
mListener = progressListener;
}
...
public void onLoginButtonClicked (View view) {
String username = mUsername.getText().toString();
String password = mPassword.getText().toString();
// validate credentials for blanks and so on
// show progress and call parse login in background method
showProgress();
ParseUser.logInInBackground(username,password, new LogInCallback() {
@Override
public void done(ParseUser parseUser, ParseException e) {
dismissProgress();
if (e == null){
// Success!, continue to MainActivity via intent
Intent intent = new Intent (LoginActivity.this, MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);
}
else {
// login failed dialog or similar.
}
}
});
}
private void showProgress() {
// show the progress and notify the listener
...
notifyListener(mListener);
}
private void dismissProgress() {
// hide the progress and notify the listener
...
notifyListener(mListener);
}
public boolean isInProgress() {
// return true if progress is visible
}
private void notifyListener(ProgressListener listener) {
if (listener == null){
return;
}
if (isInProgress()){
listener.onProgressShown();
}
else {
listener.onProgressDismissed();
}
}
}
然后,只需执行 IdlingResource class and override its methods to communicate when the resource goes from busy to idle through its ResourceCallBack
public class ProgressIdlingResource implements IdlingResource {
private ResourceCallback resourceCallback;
private LoginActivity loginActivity;
private LoginActivity.ProgressListener progressListener;
public ProgressIdlingResource(LoginActivity activity){
loginActivity = activity;
progressListener = new LoginActivity.ProgressListener() {
@Override
public void onProgressShown() {
}
@Override
public void onProgressDismissed() {
if (resourceCallback == null){
return ;
}
//Called when the resource goes from busy to idle.
resourceCallback.onTransitionToIdle();
}
};
loginActivity.setProgressListener (progressListener);
}
@Override
public String getName() {
return "My idling resource";
}
@Override
public boolean isIdleNow() {
// the resource becomes idle when the progress has been dismissed
return !loginActivity.isInProgress();
}
@Override
public void registerIdleTransitionCallback(ResourceCallback resourceCallback) {
this.resourceCallback = resourceCallback;
}
}
最后一步是在测试的 setUp()
方法中注册您的自定义空闲资源:
Espresso.registerIdlingResources(new ProgressIdlingResource((LoginActivity) getActivity()));
就是这样!现在 espresso 将等待您的登录过程完成,然后继续所有其他测试。
如果我不够清楚或者这正是您所需要的,请告诉我。
另一种方法是使用可以检查您的 activity 的自定义空闲资源。我在这里创建了一个:
public class RequestIdlingResource implements IdlingResource {
private ResourceCallback resourceCallback;
private boolean isIdle;
@Override
public String getName() {
return RequestIdlingResource.class.getName();
}
@Override
public boolean isIdleNow() {
if (isIdle) return true;
Activity activity = getCurrentActivity();
if (activity == null) return false;
idlingCheck(activity);
if (isIdle) {
resourceCallback.onTransitionToIdle();
}
return isIdle;
}
private Activity getCurrentActivity() {
final Activity[] activity = new Activity[1];
java.util.Collection<Activity> activities = ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED);
activity[0] = Iterables.getOnlyElement(activities);
return activity[0];
}
@Override
public void registerIdleTransitionCallback(
ResourceCallback resourceCallback) {
this.resourceCallback = resourceCallback;
}
public void idlingCheck(Activity activity)
{
/*
Look up something (view or method call) on the activity to determine if it is idle or busy
*/
}
}
https://gist.github.com/clivejefferies/2c8701ef70dd8b30cc3b62a3762acdb7
我从这里得到了灵感,展示了如何在测试中使用它:
好处是您不必在实施中添加任何测试代码class。
上面的答案对于 2020 年来说似乎有点过时了。
现在不需要自己创建 CountingIdlingResource。已经有一个了。您可以创建它的单例实例并在您的 activity 代码中访问它:
// CountingIdlingResourceSingleton.kt:
import androidx.test.espresso.idling.CountingIdlingResource
object CountingIdlingResourceSingleton {
private const val RESOURCE = "GLOBAL"
@JvmField val countingIdlingResource = CountingIdlingResource(RESOURCE)
fun increment() {
countingIdlingResource.increment()
}
fun decrement() {
if (!countingIdlingResource.isIdleNow) {
countingIdlingResource.decrement()
}
}
}
然后在您的应用程序代码中像这样使用它:
// MainActivity.kt:
start_activity_button.setOnClickListener {
val intent = Intent(context, LoginActivity::class.java)
CountingIdlingResourceSingleton.increment()
// I am using a kotlin coroutine to simulate a 3 second network request:
val job = GlobalScope.launch {
// our network call starts
delay(3000)
}
job.invokeOnCompletion {
// our network call ended!
CountingIdlingResourceSingleton.decrement()
startActivity(intent)
}
}
然后在测试中注册你的空闲资源:
// LoginTest.kt:
@Before
fun registerIdlingResource() {
IdlingRegistry.getInstance().register(CountingIdlingResourceSingleton.countingIdlingResource)
}
@After
fun unregisterIdlingResource() {
IdlingRegistry.getInstance().unregister(CountingIdlingResourceSingleton.countingIdlingResource)
}
您可以在我的博客 post 上找到关于 how to make espresso wait for network calls
的更多信息
我正在尝试使用 Espresso 来测试我的 UI。当我登录我的应用程序时,我调用 Parse API(网络调用)来验证用户名和密码。如果一切顺利,用户将被定向到一个新的 activity。我想对此进行测试,但我似乎无法使用空闲资源。
代码:
public class ApplicationTest extends ActivityInstrumentationTestCase2<LoginActivity> {
private CountingIdlingResource fooServerIdlingResource;
public ApplicationTest() {
super(LoginActivity.class);
}
@Before
public void setUp() throws Exception {
super.setUp();
injectInstrumentation(InstrumentationRegistry.getInstrumentation());
getActivity();
CountingIdlingResource countingResource = new CountingIdlingResource("FooServerCalls");
this.fooServerIdlingResource = countingResource;
Espresso.registerIdlingResources(countingResource);
}
public void testChangeText_sameActivity() {
// Type text and then press the button.
onView(withId(R.id.username))
.perform(typeText("s@s.nl"), closeSoftKeyboard());
onView(withId(R.id.password))
.perform(typeText("s"), closeSoftKeyboard());
if(performClick())
onView(withId(R.id.main_relative_layout))
.check(matches(isDisplayed()));
// Check that the text was changed.
}
public boolean performClick(){
fooServerIdlingResource.increment();
try {
onView(withId(R.id.login)).perform(click());
return true;
} finally {
fooServerIdlingResource.decrement();
}
}
@SuppressWarnings("javadoc")
public final class CountingIdlingResource implements IdlingResource {
private static final String TAG = "CountingIdlingResource";
private final String resourceName;
private final AtomicInteger counter = new AtomicInteger(0);
private final boolean debugCounting;
// written from main thread, read from any thread.
private volatile ResourceCallback resourceCallback;
// read/written from any thread - used for debugging messages.
private volatile long becameBusyAt = 0;
private volatile long becameIdleAt = 0;
/**
* Creates a CountingIdlingResource without debug tracing.
*
* @param resourceName the resource name this resource should report to Espresso.
*/
public CountingIdlingResource(String resourceName) {
this(resourceName, false);
}
/**
* Creates a CountingIdlingResource.
*
* @param resourceName the resource name this resource should report to Espresso.
* @param debugCounting if true increment & decrement calls will print trace information to logs.
*/
public CountingIdlingResource(String resourceName, boolean debugCounting) {
this.resourceName = checkNotNull(resourceName);
this.debugCounting = debugCounting;
}
@Override
public String getName() {
return resourceName;
}
@Override
public boolean isIdleNow() {
return counter.get() == 0;
}
@Override
public void registerIdleTransitionCallback(ResourceCallback resourceCallback) {
this.resourceCallback = resourceCallback;
}
/**
* Increments the count of in-flight transactions to the resource being monitored.
* <p/>
* This method can be called from any thread.
*/
public void increment() {
int counterVal = counter.getAndIncrement();
if (0 == counterVal) {
becameBusyAt = SystemClock.uptimeMillis();
}
if (debugCounting) {
Log.i(TAG, "Resource: " + resourceName + " in-use-count incremented to: " + (counterVal + 1));
}
}
/**
* Decrements the count of in-flight transactions to the resource being monitored.
* <p/>
* If this operation results in the counter falling below 0 - an exception is raised.
*
* @throws IllegalStateException if the counter is below 0.
*/
public void decrement() {
int counterVal = counter.decrementAndGet();
if (counterVal == 0) {
// we've gone from non-zero to zero. That means we're idle now! Tell espresso.
if (null != resourceCallback) {
resourceCallback.onTransitionToIdle();
}
becameIdleAt = SystemClock.uptimeMillis();
}
if (debugCounting) {
if (counterVal == 0) {
Log.i(TAG, "Resource: " + resourceName + " went idle! (Time spent not idle: " +
(becameIdleAt - becameBusyAt) + ")");
} else {
Log.i(TAG, "Resource: " + resourceName + " in-use-count decremented to: " + counterVal);
}
}
checkState(counterVal > -1, "Counter has been corrupted!");
}
/**
* Prints the current state of this resource to the logcat at info level.
*/
public void dumpStateToLogs() {
StringBuilder message = new StringBuilder("Resource: ")
.append(resourceName)
.append(" inflight transaction count: ")
.append(counter.get());
if (0 == becameBusyAt) {
Log.i(TAG, message.append(" and has never been busy!").toString());
} else {
message.append(" and was last busy at: ")
.append(becameBusyAt);
if (0 == becameIdleAt) {
Log.w(TAG, message.append(" AND NEVER WENT IDLE!").toString());
} else {
message.append(" and last went idle at: ")
.append(becameIdleAt);
Log.i(TAG, message.toString());
}
}
}
}
}
我现在得到的异常如下:
ndroid.support.test.espresso.IdlingResourceTimeoutException: Wait for [FooServerCalls] to become idle timed out
当我 运行 测试时,用户名和密码正在填写,但从未调用执行点击,几秒钟后我收到异常。如何正确实现空闲资源?
编辑 --
我建议 Android 使用 Calabash。 Calabash 的工作原理类似,但不需要您更改应用程序代码来进行测试。
Espresso 将在执行点击(或任何查看操作)之前轮询空闲资源。但是在点击 之后,您不会减少计数器。这是一个僵局。
我在这里看不到任何快速修复;你的方法对我来说真的没有意义。我想到了一些可能的替代方法:
- 根据您用于网络的库,您或许可以编写一个空闲资源来检查是否有正在进行的呼叫。
- 如果您在调用登录时显示进度指示器,则可以安装一个等待它消失的 IdlingResource。
- 您可以等待下一个 activity 启动,或者等待某个视图 appear/disappear。
正如其他答案所暗示的那样,countingIdlingResource 并不真正适用于您的用例。
我经常做的是添加一个接口 - 让我们称之为 ProgressListener
- 作为 activity / 片段的一个字段,它有一个要等待的资源(异步后台工作,更长网络会话等)以及每次显示或取消进度时通知它的方法。
我假设您有凭据验证逻辑和对 LoginActivity
中的 Parse API 的调用,如果成功,它将调用对 MainActivity
的意图。
public class LoginActivity extends AppCompatActivity {
private ProgressListener mListener;
...
public interface ProgressListener {
public void onProgressShown();
public void onProgressDismissed();
}
public void setProgressListener(ProgressListener progressListener) {
mListener = progressListener;
}
...
public void onLoginButtonClicked (View view) {
String username = mUsername.getText().toString();
String password = mPassword.getText().toString();
// validate credentials for blanks and so on
// show progress and call parse login in background method
showProgress();
ParseUser.logInInBackground(username,password, new LogInCallback() {
@Override
public void done(ParseUser parseUser, ParseException e) {
dismissProgress();
if (e == null){
// Success!, continue to MainActivity via intent
Intent intent = new Intent (LoginActivity.this, MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);
}
else {
// login failed dialog or similar.
}
}
});
}
private void showProgress() {
// show the progress and notify the listener
...
notifyListener(mListener);
}
private void dismissProgress() {
// hide the progress and notify the listener
...
notifyListener(mListener);
}
public boolean isInProgress() {
// return true if progress is visible
}
private void notifyListener(ProgressListener listener) {
if (listener == null){
return;
}
if (isInProgress()){
listener.onProgressShown();
}
else {
listener.onProgressDismissed();
}
}
}
然后,只需执行 IdlingResource class and override its methods to communicate when the resource goes from busy to idle through its ResourceCallBack
public class ProgressIdlingResource implements IdlingResource {
private ResourceCallback resourceCallback;
private LoginActivity loginActivity;
private LoginActivity.ProgressListener progressListener;
public ProgressIdlingResource(LoginActivity activity){
loginActivity = activity;
progressListener = new LoginActivity.ProgressListener() {
@Override
public void onProgressShown() {
}
@Override
public void onProgressDismissed() {
if (resourceCallback == null){
return ;
}
//Called when the resource goes from busy to idle.
resourceCallback.onTransitionToIdle();
}
};
loginActivity.setProgressListener (progressListener);
}
@Override
public String getName() {
return "My idling resource";
}
@Override
public boolean isIdleNow() {
// the resource becomes idle when the progress has been dismissed
return !loginActivity.isInProgress();
}
@Override
public void registerIdleTransitionCallback(ResourceCallback resourceCallback) {
this.resourceCallback = resourceCallback;
}
}
最后一步是在测试的 setUp()
方法中注册您的自定义空闲资源:
Espresso.registerIdlingResources(new ProgressIdlingResource((LoginActivity) getActivity()));
就是这样!现在 espresso 将等待您的登录过程完成,然后继续所有其他测试。
如果我不够清楚或者这正是您所需要的,请告诉我。
另一种方法是使用可以检查您的 activity 的自定义空闲资源。我在这里创建了一个:
public class RequestIdlingResource implements IdlingResource {
private ResourceCallback resourceCallback;
private boolean isIdle;
@Override
public String getName() {
return RequestIdlingResource.class.getName();
}
@Override
public boolean isIdleNow() {
if (isIdle) return true;
Activity activity = getCurrentActivity();
if (activity == null) return false;
idlingCheck(activity);
if (isIdle) {
resourceCallback.onTransitionToIdle();
}
return isIdle;
}
private Activity getCurrentActivity() {
final Activity[] activity = new Activity[1];
java.util.Collection<Activity> activities = ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED);
activity[0] = Iterables.getOnlyElement(activities);
return activity[0];
}
@Override
public void registerIdleTransitionCallback(
ResourceCallback resourceCallback) {
this.resourceCallback = resourceCallback;
}
public void idlingCheck(Activity activity)
{
/*
Look up something (view or method call) on the activity to determine if it is idle or busy
*/
}
}
https://gist.github.com/clivejefferies/2c8701ef70dd8b30cc3b62a3762acdb7
我从这里得到了灵感,展示了如何在测试中使用它:
好处是您不必在实施中添加任何测试代码class。
上面的答案对于 2020 年来说似乎有点过时了。 现在不需要自己创建 CountingIdlingResource。已经有一个了。您可以创建它的单例实例并在您的 activity 代码中访问它:
// CountingIdlingResourceSingleton.kt:
import androidx.test.espresso.idling.CountingIdlingResource
object CountingIdlingResourceSingleton {
private const val RESOURCE = "GLOBAL"
@JvmField val countingIdlingResource = CountingIdlingResource(RESOURCE)
fun increment() {
countingIdlingResource.increment()
}
fun decrement() {
if (!countingIdlingResource.isIdleNow) {
countingIdlingResource.decrement()
}
}
}
然后在您的应用程序代码中像这样使用它:
// MainActivity.kt:
start_activity_button.setOnClickListener {
val intent = Intent(context, LoginActivity::class.java)
CountingIdlingResourceSingleton.increment()
// I am using a kotlin coroutine to simulate a 3 second network request:
val job = GlobalScope.launch {
// our network call starts
delay(3000)
}
job.invokeOnCompletion {
// our network call ended!
CountingIdlingResourceSingleton.decrement()
startActivity(intent)
}
}
然后在测试中注册你的空闲资源:
// LoginTest.kt:
@Before
fun registerIdlingResource() {
IdlingRegistry.getInstance().register(CountingIdlingResourceSingleton.countingIdlingResource)
}
@After
fun unregisterIdlingResource() {
IdlingRegistry.getInstance().unregister(CountingIdlingResourceSingleton.countingIdlingResource)
}
您可以在我的博客 post 上找到关于 how to make espresso wait for network calls
的更多信息