2

I have a need to use a different implementation class for an interface when I'm testing an Android Activity than when it runs in production. I'm familiar with using mock objects, but the problem is that an Activity on Android is instantiated by the OS and not by me, so by the time I get a reference to the Activity, it has already been instantiated and already has used the implementation class. In my case, the class I'm talking about is a Login class. For my automated tests, I want to be able to control the login result. Here is my example:

class MainActivity extends Activity {

    public void onCreate(Bundle savedState) {
        super.onCreate(savedState);
        setContentView(R.layout.main);
        LoginMgr aLoginMgr = new LoginMgrImpl();
        if (aLoginMgr.authenticated()) {
            //do something
        } else {
            //do something else
        }
    }
}


public interface LoginMgr {
    public boolean authenticated();
}

Elsewhere in a library I have an implementation class:

public class LoginMgrImpl implements LoginMgr {
    //authenticate
}

During an automated test, I want to be able to replace the production LoginMgrImpl with a test version that will return the boolean I need for my test. This is where I need help as I cannot see how I can get the MainActivityto use a different LoginMgrImpl. I'm using Eclipse if that matters. The ActivityInstrumentationTestCase2<MainActivity> test class creates the MainActivity for me when I call getActivity(). It calls the no arg constructor, so I don't have a chance to change LoginMgrImpl there. Eclipse controls the build, so I don't see a way to have MainActivity built with a different implementation library.

Can any of you more experienced folks point me in a direction to successfully automate my test? I can't be the only one trying to mock some test classes used by Activity objects, but I've spent hours trying to find a solution in the forums with no success. HELP!


Based on feedback from everyone, I tried various solutions and found two that I can live with.

SOLUTION 1: One is explained in the marked answer and involves setting up a separate "Project" in Eclipse that has symbolic links to the original src, res, assets folders as well as to the AndroidManifest.xml. I can then modify the project properties for the linked version of the project to reference a different library implementation that supports my test. This works, but feels kludgy.


SOLUTION 2: Define an Application subclass for my project which keeps a singleton instance of the implementation of LoginMgr. The MainActivity will then retrieve the LoginMgr from the Application instance. During testing I can use ActivityUnitTestCase<MainActivity> to inject a mock Application that references a mock LoginMgr. Here are the code snippets:

Main Application: (be sure AndroidManifest.xml includes <Application android:name="MainApplication" ...> )

class MainApplication extends Application {

    private static LoginMgr sLoginMgr = new LoginMgrImpl();

    public LoginMgr getLoginMgr() {
        return sLoginMgr;
    }

}

MainActivity has to be modified as:

class MainActivity extends Activity {

    public void onCreate(Bundle savedState) {
        super.onCreate(savedState);
        setContentView(R.layout.main);

        // 2 lines added
        MainApplication aApplication = (MainApplication)getApplication();
        LoginMgr aLoginMgr = aApplication.getLoginMgr();

        if (aLoginMgr.authenticated()) {
            //do something
        } else {
            //do something else
        }
    }
}

The unit test must be something like this:

public class MainActivityTest extends ActivityUnitTestCase<MainActivity> {

  public MainActivityTest() {
    super(MainActivity.class);
  }

  public void testUseAlternateLoginMgr() {
    MainApplication aMockApplication = new MainApplication()
    {
      @Override
      public LoginMgr getLoginMgr() {
        return new LoginMgr() {
          public boolean authenticated() {              {
            return true;
          }
        };
      }
    };

    setApplication(aMockApplication);

    Intent aDependencyIntent = new Intent("android.intent.action.MAIN");
    Activity aMainActivity = startActivity(aDependencyIntent, null, null);
    // verify results
  }
}

I would probably use a mock framework instead of a hand coded mock, but this is just an illustration.

I think solution 2 is the better way to go. Thanks @yorkw!

TWiStErRob
  • 44,762
  • 26
  • 170
  • 254
Chuck Krutsinger
  • 2,830
  • 4
  • 28
  • 50
  • One improvement could be create public setLoginMgr() in MainApplication for test purpose only (to injecting MockLoginMgr), so in ActivityUnitTestCase, use `mainApplication.setLoginMgr(new MockMockLoginMgr());` instead of overriding getLoginMgr(). – yorkw May 11 '12 at 22:34
  • That won't work, which is what put me on this quest in the first place. Android OS instantiates an Activity for you using the no argument constructor. The LoginMgr us used in the onCreate() method before I receive a reference from Android to my own Activity. As a result, the production LoginMgr would have already executed before I could inject a different one for testing. Using the mock Application did the trick. – Chuck Krutsinger May 13 '12 at 22:30
  • According to your ActivityUnitTestCase implementation, theoretically, there should be no difference by using `mainApplication.setLoginMgr(new MockMockLoginMgr());` and the way you used (overriding), before the line where you calling `setApplication(aMockApplication);` It didn't work before because you use ActivityInstrumentationTestCase2. By using ActivityUnitTestCase, you now have complete control of how the mock application is constructed/initialized before injecting it to InstrumentationTestRunner, and calling startActivity() to start your Activity life cycle under test. – yorkw May 13 '12 at 22:57
  • The problem is there is no instance of mainApplication that exists until I call startActivity() and once I do call startActivity() the real LoginMgr has already been called instead of the mock. What ActivityUnitTestCase let me do was use a mock Application object that could pass the mock LoginMgr to my Activity as it was being created. – Chuck Krutsinger May 14 '12 at 02:24

5 Answers5

1

When I've needed to mock or stub-out certain objects in an Android app, I've isolated the creation of the object needing stubbed to a simple, single location in the Application class; then just manually comment or un-comment which version I want depending on whether it's a test or not. That's definitely not automated and it isn't really ideal, either, but it worked.

For example, have your Application class own the (static) instance of LoginMgr and make it accessible via a static method. You can use some kind of configuration data to tell the Application class which implementation to use. One way to pass configuration data into the application is via a <meta-data> node in the manifest, as described here. You might even be able to automate this by declaring a special "Testing" activity that includes the <meta-data> node (as opposed to the node being a child of the <application>), and using getPackageManager().getActivityInfo(getComponentName(), PackageManager.GET_META_DATA) to get at it from the activity. Then your Eclipse launch configuration would specify this "testing" activity to launch instead of the regular "default" activity.

E-Riz
  • 31,431
  • 9
  • 97
  • 134
  • I'm very much trying to avoid any manual steps in the testing process. As well, I don't want code in the project that is used only for testing and not for the production app. I am not presently using an Application class and I'm not sure how I would use it. Can you illustrate? – Chuck Krutsinger May 09 '12 at 17:06
  • I have just now come up with one way to test my application, but it is ugly and involves deceiving Eclipse using symbolic links in linux. I created two projects in Eclipse, Application and ApplicationTestable. Application is the real project. ApplicationTestable is a dummy project that has symbolic links to the same src, res, and assets directories as Application. I also put in a symbolic link to the AndroidManifest.xml file. I then linked ApplicationTestable to a test version of library. It works, but ugly. – Chuck Krutsinger May 09 '12 at 18:18
  • I edited my first answer to elaborate on the centralized access and configuration idea. But I also posted a separate answer about using Dependency Injection instead - I really think that idea is worth exploring, as it is part of what DI is designed for. – E-Riz May 09 '12 at 21:24
1

One way to test my application is by using symbolic links in linux to create a "testable" build of my application using a different library than the one used in production. To do this, I created two projects in Eclipse, Application and ApplicationTestable. Application is the real project. ApplicationTestable is a dummy project that has symbolic links to the same src, res, and assets directories as Application. I also put in a symbolic link to the AndroidManifest.xml file. I then linked ApplicationTestable to a test version of library by going into project properties and selecting the test version of the library instead of the production version. Then I go into the project properties of the ApplicationTest project where the jUnit tests are and have it reference the ApplicationTestable project. In this case, my test version of the library always authenticates the login, whereas the production version of the library would require a valid id and password and would attempt to authenticate against a real server. I'm still open to more elegant solutions, but I'm going forward with this one until someone offers a better idea.

Chuck Krutsinger
  • 2,830
  • 4
  • 28
  • 50
  • 1
    Android Test API is quite inflexible, ActivityInstrumentationTestCase2 can interact with real app process but dosen't support inject mock object, whereas ActivityUnitTestCase support inject mock object but doesn't interact with real app process. I usually use an application-scope boolean variable(extend android.app.application and define runInTest here) and sort of DI concept manage this, check out my answer [here](http://stackoverflow.com/questions/8295003/best-way-to-manage-the-progressdialog-from-asynctask/8317071#8317071), drawback is mock class is maintained in main project. – yorkw May 10 '12 at 22:34
  • Based on the comment above from @yorkw, I have edited my answer to use ActivityUnitTestCase combined with moving my LoginMgr implementation into the Application subclass for my app. During unit tests I can provide a mock implementation of the Application subclass which will provide a mock implementation of the LoginMgr. See last portion of my original question. – Chuck Krutsinger May 11 '12 at 20:47
1

I've implemented your first solution, but it forces me to use AndroidUnitTestCase class and there was unresolved trouble: NPE in ActionBar class getActionBar() works fine on devices/simulator but returns null in testcases

My solution is using something like this:

public class LoginHelper {
    private static LoginMgr _loginMgr ;

    public static LoginMgr get_LoginMgrImpl(){
        if(_loginMgr == null) _loginMgr = new LoginMgrImpl();
        return _loginMgr ;
    }

    public static void set_LoginMgrImpl(LoginMgr loginMgr){
        _loginMgr = loginMgr ;
    }
}

In unit-test constructor set appropriate mock implementation of interface and that's all.

public class ApplicationTest extends ActivityInstrumentationTestCase2<TestActivity> {
    TestActivity _activity;


    public ApplicationTest() {
        super(TestActivity.class);
        LoginHelper.set_LoginMgrImpl(new MockLoginMgr());
    }


    public void test1(){
        assertTrue(getActivity() != null);
    }
}
Community
  • 1
  • 1
Kolchuga
  • 716
  • 6
  • 17
0

You might want to investigate using dependency injection to solve this problem. The two DI frameworks I know of for Android are RoboGuice and Google Guice. They seem very similar, maybe even related somehow; but I haven't investigated enough to learn the differences or relationship.

E-Riz
  • 31,431
  • 9
  • 97
  • 134
0

Maybe I'm missing something elementary, but I would subclass Activity as MainActivity, then implement two constructors:

public MainActivity() {
  this(new LoginMgrImpl());
}

public MainActivity(LoginMgr loginMgr) {
  this.loginMgr = loginMgr;
}

Then I would rename LoginMgr to AuthenticationService. Then I would rename LoginMgrImpl to BlahBlahBlahAuthenticationService, where "BlahBlahBlah" describes how I implement authentication. (InMemoryAuthenticationService? LdapAuthenticationService?)

J. B. Rainsberger
  • 1,193
  • 9
  • 23
  • Maybe I'm the one who is missing something. How would the MainActivity(LoginMgr loginMgr) constructor ever get called? Activities are instantiated by the Android OS and always use the default constructor. – Chuck Krutsinger May 16 '12 at 22:34
  • Android invokes `MainActivity()`, which invokes `MainActivity(new LoginMgrImpl())`. Your tests only invoke `MainActivity(mockLoginMgr)`. Google "chaining constructors in Java". – J. B. Rainsberger May 17 '12 at 11:47
  • I think I see a problem: in my head, I'm writing straight tests, and not `ActivityUnitTestCase` tests. As long as you write your tests to use `startActivity`, you won't be able to substitute your own `LoginMgr` in the test. For this reason, I typically avoid these customised test case classes: they usually do something that appears helpful on the surface, but merely tries to bind me even more to the framework, and I want to escape the framework as soon as I can. – J. B. Rainsberger May 17 '12 at 11:56
  • I definitely understand how to chain constructors. As you just mentioned, my challenge has been how to inject something while still allowing Android OS to do the construction. The reason is that there are other injections that Android OS does that I either can't or at least don't know how to do for myself. If I instantiate an Activity myself, it won't have a context for opening files, opening dialogs, etc. – Chuck Krutsinger May 17 '12 at 14:04
  • Perhaps there are some portions of functionality I can test without needing that context, but definitely there are others that I won't be able to test without that context from the OS. I am finding the Android framework very challenging compared to my past experience developing desktop and server programs! – Chuck Krutsinger May 17 '12 at 14:05
  • By this point, in your shoes, I'd have just made `MainActivity` as thin as possible, left it untested, and test-driven everything else underneath. – J. B. Rainsberger May 17 '12 at 19:53