web-dev-qa-db-fra.com

Comment utiliser Espresso Idling Resource pour les appels réseau

J'essaie d'utiliser Espresso pour tester mon interface utilisateur. Lorsque je me connecte à mon application, j'appelle l'API Parse (appel réseau) pour vérifier le nom d'utilisateur et le mot de passe. Si tout va bien, l'utilisateur est dirigé vers une nouvelle activité. Je veux tester cela, mais je ne peux pas sembler travailler avec la ressource inactive.

Code:

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());
            }
        }
    }
}

}

L'exception que je reçois maintenant est la suivante:

ndroid.support.test.espresso.IdlingResourceTimeoutException: Wait for [FooServerCalls] to become idle timed out

Lorsque j'exécute le test, le nom d'utilisateur et le mot de passe sont remplis, mais le clic d'exécution n'est jamais appelé et j'obtiens l'exception après quelques secondes. Comment dois-je implémenter correctement la ressource inactive?

ÉDITER --

Je recommanderais d'utiliser Calabash pour Android. La calebasse fonctionne de la même manière, mais vous n'avez pas besoin de modifier le code de votre application pour le tester.

18
Dennis Anderson

Comme l'autre réponse le suggère, le countingIdlingResource ne s'applique pas vraiment à votre cas d'utilisation.

Ce que je fais toujours, c'est ajouter une interface - appelons celle-ci ProgressListener - comme champ de l'activité/du fragment qui a une ressource à attendre (travail en arrière-plan asynchrone, sessions de mise en réseau plus longues, etc.) et un pour l'avertir chaque fois que la progression est affichée ou rejetée.

Je suppose que vous avez votre logique de validation des informations d'identification et l'appel à l'API Parse dans le LoginActivity, qui appellera ensuite une intention au MainActivity en cas de succès.

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();
        }
    }
}

Ensuite, implémentez simplement la classe IdlingResource et remplacez ses méthodes pour communiquer lorsque la ressource passe d'occupé à inactif via son 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;
    }
}

La dernière étape consiste à enregistrer votre ressource de ralenti personnalisée dans la méthode setUp() du test:

Espresso.registerIdlingResources(new ProgressIdlingResource((LoginActivity) getActivity()));

Et c'est tout! Espresso attendra maintenant la fin de votre processus de connexion, puis poursuivra tous les autres tests.

Veuillez me faire savoir si je n'ai pas été suffisamment clair ou si c'est exactement ce dont vous aviez besoin.

24
appoll

Espresso interrogera la ressource inactive juste avant d'effectuer le clic (ou toute action d'affichage). Mais vous ne décrémentez votre compteur que après le clic. C'est une impasse.

Je ne vois aucune solution miracle ici; votre approche n'a pas vraiment de sens pour moi. Quelques approches alternatives possibles viennent à l'esprit:

  • Selon la bibliothèque que vous utilisez pour la mise en réseau, vous pourrez peut-être écrire une ressource inactive qui vérifie s'il y a un appel en cours.
  • Si vous affichez un indicateur de progression pendant l'appel de connexion, vous pouvez installer un IdlingResource qui attend que cela disparaisse.
  • Vous pouvez attendre que la prochaine activité soit lancée ou qu'une certaine vue apparaisse/disparaisse.
2
Daniel Lubarov

Une autre approche consiste à avoir une ressource de ralenti personnalisée qui peut examiner votre activité. J'en ai créé un ici:

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

Je me suis inspiré d'ici, ce qui montre comment il pourrait être utilisé dans un test:

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

La bonne chose est que vous n'avez pas besoin d'ajouter de code de test à votre classe d'implémentation.

1
Clive Jefferies

Les réponses ci-dessus semblent un peu dépassées pour 2020. De nos jours, vous n'avez pas besoin de créer vous-même le CountingIdlingResource. Il y en a déjà un. Vous pouvez en créer une instance singleton et y accéder dans votre code d'activité:

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

Ensuite, utilisez-le comme ceci dans votre code d'application:

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

Enregistrez ensuite votre ressource inactive dans le test:

// LoginTest.kt: 
@Before
fun registerIdlingResource() {
    IdlingRegistry.getInstance().register(CountingIdlingResourceSingleton.countingIdlingResource)
}

@After
fun unregisterIdlingResource() {
    IdlingRegistry.getInstance().unregister(CountingIdlingResourceSingleton.countingIdlingResource)
}

Vous pouvez trouver des informations supplémentaires sur mon article de blog sur comment faire espresso attendre les appels résea

0
stoefln