web-dev-qa-db-ja.com

Espresso Idling Resourceをネットワークコールに使用する方法

Espressoを使用してUIをテストしようとしています。アプリケーションにログインするときに、Parse APIへの呼び出し(ネットワーク呼び出し)を行って、ユーザー名とパスワードを確認します。すべてが順調であれば、ユーザーは新しいアクティビティに移動します。これをテストしたいのですが、アイドル状態のリソースで動作するようには思えません。

コード:

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("[email protected]"), 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は同様に機能しますが、テストのためにアプリのコードを変更する必要はありません。

18
Dennis Anderson

他の回答が示唆するように、countingIdlingResourceは実際のユースケースには適用されません。

私が常に行うことは、インターフェイスを追加することです-これをProgressListenerと呼びましょう-待機するリソース(非同期のバックグラウンド作業、より長いネットワークセッションなど)を持つアクティビティ/フラグメントのフィールドとして、進行状況が表示または却下されるたびに通知するメソッド。

資格情報検証ロジックと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 クラスを実装し、そのメソッドをオーバーライドして、リソースがそのビジー状態からアイドル状態になったときに 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はログインプロセスが完了するのを待ってから、他のすべてのテストを続行します。

私が十分に明確でなかったか、それがまさにあなたが必要としたものであるかどうかを知らせてください。

24
appoll

Espressoは、クリック(または任意の表示アクション)を実行する直前にアイドリングリソースをポーリングします。ただし、カウンターはafterクリックまで減少しません。それはデッドロックです。

ここには簡単な修正はありません。あなたのアプローチは私には本当に意味がありません。いくつかの可能な代替アプローチが思い浮かびます:

  • ネットワークに使用するライブラリによっては、進行中の呼び出しがあるかどうかを確認するアイドリングリソースを作成できる場合があります。
  • ログイン呼び出しの進行中に進行状況インジケーターを表示する場合、それが消えるのを待つIdlingResourceをインストールできます。
  • 次のアクティビティが起動するか、特定のビューが表示/非表示になるのを待つことができます。
2
Daniel Lubarov

別のアプローチは、アクティビティを調査できるカスタムアイドリングリソースを用意することです。ここで作成しました:

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

私はここからインスピレーションを得ました。それはテストでどのように使用できるかを示しています:

https://github.com/AzimoLabs/ConditionWatcher/blob/master/sample/src/androidTest/Java/com/azimolabs/f1sherkk/conditionwatcherexample/IdlingResourceExampleTests.Java

良い点は、実装クラスにテストコードを追加する必要がないことです。

1
Clive Jefferies

上記のanserは、2020年には時代遅れのようです。現在、CountingIdlingResourceを自分で作成する必要はありません。もう一つあります。そのシングルトンインスタンスを作成し、アクティビティコードでアクセスできます。

// 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)
}

エスプレッソにネットワークコールを待機させる方法 に関する追加情報が私のブログ投稿にあります

0
stoefln