Android Instrumented Testing of an Activity

Problem

From an instrumented test, I need to start an Activity that has a WebView in it and verify that sending the WebView to a particular URL results in success. If any errors are encountered by the WebView when loading the URL, I need to get that information in the instrumented test.

Solution

My solution uses a delayed-loading ActivityTestRule to specify the Activity to test and an inline instance of a special Interface implemented within the test that is registered with the Activity, once it is loaded by the instrumented test.

Activity side

Here is a simple Activity that has a WebView embedded in it’s layout that we want to test. I’ve added the necessary elements to support testing and they are called out with numbered comments which we will discuss below:

public class MyWebViewActivity extends AppCompatActivity {

    //region Private members

    /**
     * Log tag for this class.
     */
    private static final String TAG = MyWebViewActivity.class.getCanonicalName();

    /**
     * Reference to the {@link WebView} embedded in this activity layout.
     */
    private WebView webView = null;

    /**
     * The {@link WebViewClient} for trapping {@link WebView} events.
     */
    private WebViewClient webViewClient = null;

    /**
     * The {@link WebChromeClient} for trapping {@link WebView} events.
     */
    private WebChromeClient webChromeClient = null;

    /*** 1 ***/
    /**
     * A test class instance implementing {@link MyWebViewActivityInterface} used as a delegate in the instrumented tests.
     */
    private MyWebViewActivityInterface testingDelegate = null;

    //endregion

    //region Public methods

    /*** 2 ***/
    /**
     * Sets the testing delegate that implements {@link MyWebViewActivityInterface} for communicating results from this activity back to the test.
     *
     * @param delegate The test class instance which implements {@link MyWebViewActivityInterface}.
     */
    public void setTestingDelegate(MyWebViewActivityInterface delegate) {

        this.testingDelegate = delegate;
    }

    //endregion

    @Override
    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_mywebview);

        // Get a reference to the embedded WebView.
        webView = (WebView)findViewById(R.id.webView);

        // Get the Intent that started this activity, if any, and extract the url to load.
        String urlToLoad = null;
        Intent intent = getIntent();
        if (intent != null) {

            // Get the url to load.
            urlToLoad = intent.getStringExtra("url");
        }

        // Create the webview clients to track events in the webview.
        webViewClient = new WebViewClient() {

            @Override
            public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {

                super.onReceivedError(view, request, error);

                Log.e(TAG, "onReceivedError: " + error);

                /*** 3 ***/
                // If we are testing, send the results to the calling test.
                if (testingDelegate != null) {

                    testingDelegate.onWebViewError(request, error);
                }
            }

            @Override
            public void onPageStarted(WebView view, String url, Bitmap favicon) {

                super.onPageStarted(view, url, favicon);

                Log.i(TAG, String.format("onPageStarted: loading '%s'", url));
            }

            @Override
            public void onPageFinished(WebView view, String url) {

                super.onPageFinished(view, url);

                Log.i(TAG, String.format("onPageFinished: finished loading '%s'", url));

                /*** 4 ***/
                // If we are testing, send the results to the calling test.
                if (testingDelegate != null) {

                    testingDelegate.onFinishedLoading(url);
                }
            }
        };
        webChromeClient = new WebChromeClient() {

            @Override
            public boolean onJsAlert(WebView view, String url, String message, JsResult result) {

                Log.i(TAG, String.format("onJsAlert: javascript alert '%s'", url));

                return super.onJsAlert(view, url, message, result);
            }

            @Override
            public boolean onConsoleMessage(ConsoleMessage consoleMessage) {

                Log.i(TAG, String.format("onConsoleMessage: console message: '%s'", consoleMessage.toString()));

                return super.onConsoleMessage(consoleMessage);
            }

            @Override
            public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {

                Log.i(TAG, String.format("onJsPrompt: javascript prompt '%s' url '%s'", message, url));

                return super.onJsPrompt(view, url, message, defaultValue, result);
            }
        };

        // Assign the clients for webview event trapping.
        webView.setWebViewClient(webViewClient);
        webView.setWebChromeClient(webChromeClient);

        // Load the url, if specified.
        if (urlToLoad != null) {

            webView.loadUrl(urlToLoad.toString());
        }
    }
}    

Let’s discuss each numbered section of code:

1: We define a private member to hold an instance of a testing delegate that implements an interface which we can use to callback into the test when certain events occur. For our example, the interface looks like:

public interface MyWebViewActivityInterface {

    /**
     * The WebView encountered an error when trying to load the specified URL.
     *
     * @param request The resource request that triggered the error.
     * @param error The error that was encountered.
     */
    public void onWebViewError(WebResourceRequest request, WebResourceError error);

    /**
     * The WebView successfully loaded the specified URL.
     *
     * @param url The URL loaded.
     */
    public void onFinishedLoading(String url);
}

2: We provide a public method to set the testing delegate member in #1 above so we can set this from the test when we have a valid reference to the Activity.

3: If there is an error loading the URL and our testingDelegate reference is non-null, we call the interface method onWebViewError (see #1 above) with the request and the error. This will call back into the test code where we can notify a test expectation with these values and continue the test.

4: When the URL is successfully loaded, and our testingDelegate reference is non-null, we call the interface method onFinishedLoading (see #1 above) with the URL that was loaded.

Of course, this is only an example and you could add more callback methods to the interface and use the same code in #3 or #4 in other parts of the Activity code like in the WebViewClient and/or WebChromeClient to communicate other specific events during the load of the page back to the test.

Test side

The instrumented test must be able to start the Activity and send it the URL to load and register our testing delegate instance so the callbacks will happen.

We define an ActivityTestRule as a public (it must be public) member of our test class:

@Rule
public ActivityTestRule<MyWebViewActivity> mActivityRule = new ActivityTestRule<>(MyWebViewActivity.class, true, false /* Do not start the activity right away */);

This rule defines the Activity to load, in our case MyWebViewActivity. Notice that the third parameter is false – this causes the ActivityTestRule to not launch our Activity initially, allowing us to manually launch it in each test, which we want to do so we can send along an Intent with a URL to load.

To create an Intent that will load the Activity, in my example the MyWebViewActivity, we need the Application context. But how do we get it from inside an instrumented test? We can use the getTargetContext() static method of the InstrumentationRegistry class to get it:

Context context = InstrumentationRegistry.getTargetContext();

With this we can then use the ActivityTestRule member to launch MyWebViewActivity and send it some data to start testing:

// Tell MyWebViewActivity to start and load the url specified.
Intent intent = new Intent(context, MyWebViewActivity.class);
intent.putExtra("url", "http://code.chrisdisdero.com");
mActivityRule.launchActivity(intent);

The ActivityTestRule method launchActivity pauses our test code until the activity is open, so we can be guaranteed that after this call, we can get a valid reference to the Activity. We do this next and register our testing delegate instance with the Activity:

// Get the activity.
MyWebViewActivity activity = mActivityRule.getActivity();

// Set the testing delegate so we can test the result of loading the page in the WebView.
activity.setTestingDelegate(new MyWebViewActivityInterface() {

  @Override
  public void onWebViewError(WebResourceRequest request, WebResourceError error) {

    // There was an error loading the web page.
    expectation.put("request", request);
    expectation.put("error", error);
    expectation.fulfill(CRDTestExpectationStatus.FAILURE);
  }

  @Override
  public void onFinishedLoading(String url) {

    // The web page was successfully loaded.
    expectation.put("url", url);
    expectation.fulfill(CRDTestExpectationStatus.SUCCESS);
  }
});

Remember the code sections #3 and #4 in the MyWebViewActivity where we callback to the test via the testing delegate registered with the Activity when the URL loaded successfully, and when there’s an error if the WebViewClient onReceivedError() callback is triggered. This is the block of code in the test where we get called back from the Activity.

We can use a combination of wait() and notify() in our instrumented test to wait for the callbacks to be received from the Activity before asserting that the test results are expected or not. I have a convenient code library called CRDTestExpectation that you can easily add to your Android project which provides a wrapper for these calls and also provides some additional features which make it a great test expectation solution.

Here’s the complete instrumented test code to test the example MyWebViewActivity shown above, complete with the use of my CRDTestExpectation library for test expectations:

/**
 * Instrumentation test for testing the {@link MyWebViewActivity}.
 */
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {

    //region Private members

    /**
     * Maximum time to wait for the test expectation.
     */
    private final long timeoutMilliseconds = 20000;

    //endregion

    //region Rules

    @Rule
    public ActivityTestRule<MyWebViewActivity> mActivityRule = new ActivityTestRule<>(MyWebViewActivity.class, true, false /* Do not start the activity right away */);

    //endregion

    //region Tests

    @Test
    public void testMyWebViewActivity() throws Exception {

        // Create expectation to wait for MyWebViewActivity to load the page we will specify below.
        final CRDTestExpectation expectation = new CRDTestExpectation();

        // Get the application context of the application under test.
        Context context = InstrumentationRegistry.getTargetContext();

        // Tell MyWebViewActivity to start and load the url specified.
        Intent intent = new Intent(context, MyWebViewActivity.class);
        intent.putExtra("url", "http://code.chrisdisdero.com");
        mActivityRule.launchActivity(intent);

        // Get the activity.
        MyWebViewActivity activity = mActivityRule.getActivity();

        // Set the testing delegate so we can test the result of loading the page in the WebView.
        activity.setTestingDelegate(new MyWebViewActivityInterface() {

            @Override
            public void onWebViewError(WebResourceRequest request, WebResourceError error) {

                // There was an error loading the web page.
                expectation.put("request", request);
                expectation.put("error", error);
                expectation.fulfill(CRDTestExpectationStatus.FAILURE);
            }

            @Override
            public void onFinishedLoading(String url) {

                // The web page was successfully loaded.
                expectation.put("url", url);
                expectation.fulfill(CRDTestExpectationStatus.SUCCESS);
            }
        });

        // Wait for the testing delegate callbacks to fire and check the status.
        CRDTestExpectationStatus status = expectation.waitFor(timeoutMilliseconds);
        assertEquals("unexpected status", CRDTestExpectationStatus.SUCCESS, status);

        // Validate the data returned from MyWebViewActivity.
        assertNull("non-null error", expectation.get("error"));
        assertNotNull("null url loaded", expectation.get("url"));
    }

    //endregion
}

cdisdero

Software Engineer

Leave a Reply

Your email address will not be published. Required fields are marked *

To create code blocks or other preformatted text, indent by four spaces:

    This will be displayed in a monospaced font. The first four 
    spaces will be stripped off, but all other whitespace
    will be preserved.
    
    Markdown is turned off in code blocks:
     [This is not a link](http://example.com)

To create not a block, but an inline code span, use backticks:

Here is some inline `code`.

For more help see http://daringfireball.net/projects/markdown/syntax