mercredi 20 décembre 2017

Test legacy asynchronous code using Espresso

I am trying to figure out how to test a piece of asynchronous code that's embedded in a legacy Java SDK for Android. Since I am the new person responsible for maintaining the legacy SDK, I could change it to make it more testable. But I'd like ideas for how to do it without breaking any of the legacy SDK's APIs. I don't want to create work for existing users of the SDK when they upgrade to the latest version.

One of the asynchronous operations in the legacy SDK is a message that displays temporarily like a tool-tip to guide the end user. After 5 seconds the tool-tip fades away. The fading mechanism is implemented with the android.os.Handler.Callback interface:

final class PoiHint extends Fader implements Handler.Callback {
    private final Handler handler = new Handler(Looper.getMainLooper(), this);

    @Override public boolean handleMessage(Message msg) {
        switch (msg.what) {
            case MSG_SHOW:
                super.show();
                handler.sendEmptyMessageDelayed(MSG_HIDE, HIDE_DELAY_MILLIS);
                return true;
            case MSG_HIDE:
                super.hide();
                return true;
        }
        return false;
    }
}

My objective is to write an Espresso test that verifies the message displays and fades away. Ideally I'd test that it displays for 5 seconds or that it displays for not longer than a few seconds. But I'd be satisfied just to test that it displays and fades away.

If I were designing this from scratch, I would use Espresso's Idling Resource counter. I found a good model that's similar to mine, IdlingResourceSample. The similarity is my legacy SDK calls Handler.sendEmptyMessageDelayed() and IdlingResourceSample calls Handler.postDelayed():

static void processMessage(final String message, final DelayerCallback callback,
        @Nullable final SimpleIdlingResource idlingResource) {
    // The IdlingResource is null in production.
    if (idlingResource != null) {
        idlingResource.setIdleState(false);
    }

    // Delay the execution, return message via callback.
    Handler handler = new Handler();
    handler.postDelayed(new Runnable() {
        @Override
        public void run() {
            if (callback != null) {
                callback.onDone(message);
                if (idlingResource != null) {
                    idlingResource.setIdleState(true);
                }
            }
        }
    }, DELAY_MILLIS);
}

To copy IdlingResourceSample's approach I would need to pass a SimpleIdlingResource into the legacy SDK, for example into PoiHint's constructor. That constructor is 8 calls deep into the legacy SDK's API. I would either need to pass the SimpleIdlingResource down through all those calls or implement the singleton pattern which tends to be a bad idea because such code is harder to test due to a fixed dependency on a global class.

Furthermore, wouldn't I have to expose the SimpleIdlingResource to the users of my legacy SDK? After all an SDK has to be run in an Android app to be tested. In our case, we have a reference implementation app that's been the app my Espresso tests perform tests on. That reference implementation would need to initialize the legacy SDK by passing a SimpleIdlingResource so that the Espresso test code also has a reference to the SimpleIdlingResource, right?

I would like feedback on whether I'm wrong about any of my assumptions above. I would also like other ideas for how to test the tooltip and asynchronous code in general that's embedded within a legacy SDK.

Thanks in advance for your help.

Aucun commentaire:

Enregistrer un commentaire