Tuesday, July 07, 2009

Android: Testing on the Android platform - Is Toast leaking ?









This document can be read in Google Docs (http://docs.google.com/View?id=ddwc44gs_194s36s83fz), cut and paste link if you have problems accessing it.






A couple of days ago I've found an interesting post by skink on Android Developers group titled **never ever** use Toasts with Activity context. The post speaking about NotifyWithText in ApiDemos, states something like:


"...try to show any Toast, then exit NotifyWithText

demo. run it again - you will see getInstanceCount() increases leaking

Activities. repeat running demo couple of times. counter still

increases."


So the questions are:



  • Is Toast leaking Context objects ?


  • Is it a bug in Toast, in ApiDemos, in the documentation ?


  • Should we use Application Context instead ?




Let's try to find the answers using some Unit Tests as we have been investigating in previous articles in this blog.


ToastActivity


Let's recreate a oversimplified version of the Activity to display just the Toast depending on an extra parameter DISPLAY_TOAST in the Intent starting the Activity.










package com.codtech.android.training.toast;



import android.app.Activity;

import android.app.Application;

import android.content.Context;

import android.content.Intent;

import android.os.Bundle;

import android.util.Log;

import android.widget.Toast;



public class ToastActivity extends Activity {

    private static final String TAG = "ToastActivity";

    public static final String USE_ACTIVITY_CONTEXT =

        "com.codtech.android.training.toast.useActivityContext";

    public static final String DISPLAY_TOAST =

        "com.codtech.android.training.toast.displayToast";

    private Context toastContext;

    private boolean useActivityContext;

    private boolean displayToast;

    

    /** Called when the activity is first created. */

    @Override

    public void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);



        final Intent intent = getIntent();

        useActivityContext =

            intent.getBooleanExtra(USE_ACTIVITY_CONTEXT, true);

        displayToast =

            intent.getBooleanExtra(DISPLAY_TOAST, true);

        

        if ( useActivityContext ) {

            toastContext = this;

        }

        else if ( displayToast ) {

            toastContext =

                getApplication().getApplicationContext();

        }

    }

      

    

    /* (non-Javadoc)

     * @see android.app.Activity#onPause()

     */

    @Override

    protected void onResume() {

        super.onResume();

        if ( displayToast ) {

            Toast.makeText(toastContext,

                "Sample Toast: " + getInstanceCount(),

                Toast.LENGTH_SHORT).show();

        }

        else {

            Log.d(TAG, "No toast displayed");

        }

        finish();

    }





    /* (non-Javadoc)

     * @see android.app.Activity#onDestroy()

     */

    @Override

    protected void onDestroy() {

        super.onDestroy();

        Log.d(TAG, "Activity destroyed: " + this);

    }



}




ToastActivity Tests



As usual, let's create our test to.



In this very particular case we want it to run several times specified by ToastActivityTests.N, 50 actually but you can change it if you like, to see if displaying a Toast or not change things in some way.



RepeatedTestSuite



This class implements repeated tests.











package com.codtech.android.training.toast.tests;



import junit.framework.Test;

import junit.framework.TestSuite;



public class ReapeatedTestSuite extends TestSuite {

    

    public static Test repeatedSuite(Test test, int count) {

        TestSuite suite = new TestSuite("Repeated");

        

        // there's no RepeatedTest in android's junit

        for (int i=0; i<count; i++) {

            suite.addTest(test);

        }

    

        return suite;

    }

}




RepeatedActivityContextWithToast


This test will run testSingleActivityContextWithToast starting the activity displaying the Toast several times










/**

 *

 */

package com.codtech.android.training.toast.tests;



import junit.framework.Test;





/**

 * @author diego

 *

 */

public class RepeatedActivityContextWithToast extends ReapeatedTestSuite {

    public static Test suite() {

        return repeatedSuite(

            new ToastActivityTests("testSingleActivityContextWithToast"),

            ToastActivityTests.N);

    }

}





RepeatedActivityContextWithoutToast


This test will run testSingleActivityContextWithoutToast starting the activity, without displaying any Toast, several times










/**

 *

 */

package com.codtech.android.training.toast.tests;



import junit.framework.Test;





/**

 * @author diego

 *

 */

public class RepeatedActivityContextWithoutToast extends ReapeatedTestSuite {

    public static Test suite() {

        return repeatedSuite(

            new ToastActivityTests("testSingleActivityContextWithoutToast"),

            ToastActivityTests.N);


    }

}







ToastActivityTests


We decided to fail the test if the Activity instance count reaches N/4.












/**

 *

 */

package com.codtech.android.training.toast.tests;





import java.lang.reflect.Method;



import android.app.Instrumentation;

import android.content.Intent;

import android.os.Bundle;

import android.test.ActivityInstrumentationTestCase2;

import android.test.FlakyTest;

import android.test.suitebuilder.annotation.MediumTest;

import android.util.Log;



import com.codtech.android.training.toast.ToastActivity;



/**

 * @author diego

 *

 */

public class ToastActivityTests

    extends ActivityInstrumentationTestCase2<ToastActivity> {




    private static final String TAG = "ToastActivityTests";

    public static final int N = 50;



    /**

     * @param name

     */

    public ToastActivityTests(String name) {

        super("com.codtech.android.training.toast",

            ToastActivity.class);


        setName(name);

    }





    /* (non-Javadoc)

     * @see junit.framework.TestCase#setUp()

     */

    protected void setUp() throws Exception {

        super.setUp();

    }



    /* (non-Javadoc)

     * @see android.test.InstrumentationTestCase#tearDown()

     */

    protected void tearDown() throws Exception {

        super.tearDown();

    }





    @MediumTest

    public void testSingleActivityContextWithToast() {

        exersiseActivityLifecycle(intentFactory(true, true),

            "testSingleActivityContextWithToast");


    }

    

    @MediumTest

    public void testSingleApplicationContextWithToast() {

        exersiseActivityLifecycle(intentFactory(false, true),

            "testSingleApplicationContextWithToast");


    }

    

    @MediumTest

    public void testSingleActivityContextWithoutToast() {

        exersiseActivityLifecycle(intentFactory(true, false),

            "testSingleActivityContextWithoutToast");


    }

    

    @MediumTest

    public void testSingleApplicationContextWithoutToast() {

        exersiseActivityLifecycle(intentFactory(false, false),

            "testSingleApplicationContextWithoutToast");


    }

   



    /**

     * @param intent

     */

    private void exersiseActivityLifecycle(final Intent intent, final String name) {

        setActivityIntent(intent);

        final ToastActivity activity = getActivity();

        final Instrumentation instrumentation = getInstrumentation();



        // At this point, onCreate() has been called, but nothing else

        // Complete the startup of the activity

        instrumentation.callActivityOnStart(activity);

        instrumentation.callActivityOnResume(activity);

        // At this point you could test for various configuration aspects, or you could

        // use a Mock Context to confirm that your activity has made certain calls to the system

        // and set itself up properly.

        instrumentation.callActivityOnPause(activity);

        // At this point you could confirm that the activity has paused properly, as if it is

        // no longer the topmost activity on screen.

        instrumentation.callActivityOnStop(activity);



        Runtime.getRuntime().gc();

        Runtime.getRuntime().runFinalization();

        Runtime.getRuntime().gc();

      

        long aic = ToastActivity.getInstanceCount();

        assertTrue("instance count reached " + aic, aic < N/4);



        // At this point we are invoking onDestroy explicitly because we are iterating

        // and tearDown() will not be called

        instrumentation.callActivityOnDestroy(activity);

        

        // run N times, requires FlakyTest

        try {

            Method method = this.getClass().getMethod(name, new Class[] {});

            FlakyTest flakyTest = method.getAnnotation(FlakyTest.class);

            if ( flakyTest != null ) {

                assertTrue(count >= flakyTest.tolerance());

            }

        } catch (SecurityException e) {

            // TODO Auto-generated catch block

            e.printStackTrace();

        } catch (NoSuchMethodException e) {

            // TODO Auto-generated catch block

            e.printStackTrace();

        }

    }

    

    private static Intent intentFactory(boolean useActivityContext,

            boolean displayToast) {


        final Intent intent = new Intent();

        intent.setAction(Intent.ACTION_MAIN);

        intent.setClassName("com.codtect.android.training.toast",

            "com.codtect.android.training.toast.ToastActivity");


        intent.putExtra(ToastActivity.USE_ACTIVITY_CONTEXT, useActivityContext);

        intent.putExtra(ToastActivity.DISPLAY_TOAST, displayToast);

        return intent;

    }

}







Running the tests



Running RepeatedActivityContextWithoutToast


Running the tests we can verify that everything is fine.













diego@bruce:~$ adb shell am instrument -w -e class com.codtech.android.training.toast.tests.RepeatedActivityContextWithoutToast com.codtech.android.training.toast.tests/android.test.InstrumentationTestRunner



com.codtech.android.training.toast.tests.ToastActivityTests:...................

...............................


Test results for InstrumentationTestRunner=.........................................

.........

Time: 42.473



OK (50 tests)






Running RepeatedActivityContextWithToast


Running the test that displays the Toast we find a different result. Activity instance count reaches the maximum allowed and the test fails












diego@bruce:~$ adb shell am instrument -w -e class com.codtech.android.training.toast.tests.RepeatedActivityContextWithToast com.codtech.android.training.toast.tests/android.test.InstrumentationTestRunner



com.codtech.android.training.toast.tests.ToastActivityTests:...........

Failure in testSingleActivityContextWithToast:

junit.framework.AssertionFailedError: instance count reached 12

    at com.codtech.android.training.toast.tests.ToastActivityTests.

       exersiseActivityLifecycle(ToastActivityTests.java:195)


    at com.codtech.android.training.toast.tests.ToastActivityTests.

       testSingleActivityContextWithToast(ToastActivityTests.java:138)




...





Test results for InstrumentationTestRunner=............F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.F.

F.F.F.F.F.F.F.F.F.F

Time: 56.101



FAILURES!!!

Tests run: 50,  Failures: 39,  Errors: 0







Conclusion


We can see that the only difference between both tests is the Toast being displayed, and the results are completely different.

While the demonstration given in the Android Developer's thread seems correct, this seems correct too !

Comments, suggestions and corrections are gladly welcome.

If you are interested in the source code or APK just drop me a line or leave a comment in the blog.




Copyright © 2009 Diego Torres Milano. All rights reserved.




































Post a Comment