2

I'm using mockito to mock AccountManager inside an Activity test.

So, my test code is as follows:

public class PressuresListActivityUnitTest extends
    ActivityUnitTestCase<PressuresListActivity> {

// Test data.
private static final String ACCOUNT_TYPE = "com.example.android";
private static final Account ACCOUNT_1 = new Account("account1@gmail.com", ACCOUNT_TYPE);
private static final Account ACCOUNT_2 = new Account("account2@gmail.com", ACCOUNT_TYPE);
private static final Account[] TWO_ACCOUNTS = { ACCOUNT_1, ACCOUNT_2 };

@Mock
private AccountManager mMockAccountManager;

public PressuresListActivityUnitTest() {
    super(PressuresListActivity.class);
}

@Override
protected void setUp() throws Exception {
    super.setUp();

    setupDexmaker();
    // Initialize mockito.
    MockitoAnnotations.initMocks(this);
}

public void testAccountNotFound() {
    Mockito.when(mMockAccountManager.getAccounts())
            .thenReturn(TWO_ACCOUNTS);

    Intent intent = new Intent(Intent.ACTION_MAIN);
    startActivity(intent, null, null);
}

/**
 * Workaround for Mockito and JB-MR2 incompatibility to avoid
 * java.lang.IllegalArgumentException: dexcache == null
 *
 * @see <a href="https://code.google.com/p/dexmaker/issues/detail?id=2">
 *     https://code.google.com/p/dexmaker/issues/detail?id=2</a>
 */
private void setupDexmaker() {
    // Explicitly set the Dexmaker cache, so tests that use mockito work
    final String dexCache = getInstrumentation().getTargetContext().getCacheDir().getPath();
    System.setProperty("dexmaker.dexcache", dexCache);
}

And the onCreate mthod of activity that will be tested:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_pressures_list);

    AccountManager am = AccountManager.get(this);
    Account[] accounts = am.getAccounts();
    if (accounts.length > 0) {
        Log.i("TAG", "it works!");
    }
}

But when I run the test, AccountManager.getAccounts does NOT return the accounts specified in the test.

Any idea?

vitorvigano
  • 697
  • 2
  • 8
  • 18

2 Answers2

3

That's not how mockito works.

import static org.mockito.Mockito.*;
...

public void testAccountNotFound() {

    AccountManager am = mock(AccountManager.class);
    when(am.getAccounts()).thenReturn(TWO_ACCOUNTS);

    // this is how you unit test something
    assertTrue(am.getAccounts().size == 2);
}

public void testMoreRealWorldExample() {

    AccountManager am = mock(AccountManager.class);
    when(am.getAccounts()).thenReturn(TWO_ACCOUNTS);

    /* try and create an account; createNewAccount() will call
       getAccounts() to find out how many accounts there already
       are in the system, and due to the above injection, it would
       think there are already two. Thus we can test to make sure
       users cannot create three or more accounts.
     */
    boolean accountCreated = am.createNewAccount();

    // maximum two accounts are allowed, so this should return false.
    assertFalse(accountCreated);
}

You can't directly use mockito to just arbitrarily inject values in objects and then run an Activity. Mockito is for unit testing your objects, ideally with minimal references to Android-specific objects, though some references will be inevitable.

Please read the cookbook more closely as it is pretty thorough.

If you want to mock and entire Activity, you'll need to look in to Robolectric

superus8r
  • 751
  • 1
  • 11
  • 24
Jeffrey Mixon
  • 12,846
  • 4
  • 32
  • 55
  • Actually, I'm testing the Activity, not AccountManager. To be more specific, I need to test Activity behavior according with two scenarios. First, when I start the activity and there is no account, I need to call LoginActivity. Second, when I start the activity and account already exists, no call is needed. This is why I need to mock some accounts. So, IMHO your tests described above doesn't fit. If is there another way to test this scenario, let me know. Thanks! – vitorvigano Nov 14 '14 at 20:55
  • 1
    As I said, you cannot test an `Activity` like that with mockito alone. You need to look in to using Robolectric or [Robotium](https://code.google.com/p/robotium/). Those types of tests move away from "unit" testing and are considered "functional" tests. They are much more difficult to test programmatically. – Jeffrey Mixon Nov 14 '14 at 21:01
  • I've used espresso a few times. I believe that I'm confused right now. haha.. Thanks!! – vitorvigano Nov 14 '14 at 21:09
3

After some research, I finally solved the problem.

Android provides some classes to be used inside the tests, like MockContext, IsolatedContext.

http://developer.android.com/reference/android/test/mock/MockContext.html

http://developer.android.com/reference/android/test/IsolatedContext.html

To get this done, I created a subclass of ContextWrapper and overrode(??) getSystemService method.

According to the documentation:

"Proxying implementation of Context that simply delegates all of its calls to another Context. Can be subclassed to modify behavior without changing the original Context."

http://developer.android.com/reference/android/content/ContextWrapper.html

This way, I injected the original context, but modified to fit my needs, inside the Activity using a regular AndroidActivityUnitTestCase.

Check this out:

public class FakeContextWrapper extends ContextWrapper {

    private static final String ACCOUNT_TYPE = "com.example.android";

    private static final Account ACCOUNT_1 = new Account("account1@gmail.com", ACCOUNT_TYPE);
    private static final Account ACCOUNT_2 = new Account("account2@gmail.com", ACCOUNT_TYPE);

    private static final Account[] TWO_ACCOUNTS = { ACCOUNT_1, ACCOUNT_2 };

    @Mock
    private AccountManager mMockAccountManager;

    public FakeContextWrapper(Context base) {
        super(base);

        MockitoAnnotations.initMocks(this);
        Mockito.when(mMockAccountManager.getAccounts()).thenReturn(TWO_ACCOUNTS);
    }

   @Override
   public Object getSystemService(String name) {
       if (Context.ACCOUNT_SERVICE.equals(name)) {
           return mMockAccountManager;
       } else {
           return super.getSystemService(name);
       }
   }
}

Inside the test:

public void testAccountNotFound() {
    Context context = new FakeContextWrapper(getInstrumentation().getTargetContext());
    setActivityContext(context);
    Intent intent = new Intent(Intent.ACTION_MAIN);
    startActivity(intent, null, null);
    // TODO assertions.
}

Finally, the Activity under test:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_pressures_list);

    AccountManager am = AccountManager.get(this);
    Account[] accounts = am.getAccounts();
    if (accounts.length == 0) {
        // TODO call login.
    } else {
        Log.i("TAG", "it works!");
    }
}
vitorvigano
  • 697
  • 2
  • 8
  • 18
  • This is a really helpful answer, but FYI, ActivityUnitTestCase is about to become deprecated: https://code.google.com/p/android-test-kit/issues/detail?id=121#c4 – Dan J May 06 '15 at 17:52