j2emanue
j2emanue

Reputation: 62549

Unable to spy on an Android class

I have a simple android class of constants like this:

public class Consts extends BaseConstants {

    public static final String SCHEME = "http";

    // Tag used to cancel the request
    public static final String TAG_JSON = "json_obj_req";
}

there is nothing else in it so it should be simple to mock. I am calling this in my testcase:

Mockito.spy(Consts.class); ...which is failing. Below is the testcase file:

public class ApplicationTest extends ActivityInstrumentationTestCase2<MainActivity> {

  MainActivity mActivity;

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

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

      setActivityInitialTouchMode(false);

      mActivity = getActivity();
  }

  public void testUrlValid() {
      Mockito.spy(Consts.class);
  }

}

and here is the logcat output from the test case:

Running tests
Test running started
org.mockito.exceptions.base.MockitoException:
Cannot mock/spy class java.lang.Class
Mockito cannot mock/spy following:
- final classes
- anonymous classes
- primitive types

-------UPDATE:

I want to spy on my mainActivity class but i get the same Mockito exception: heres the class im testing:

import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;


public class MainActivity extends ListPageActivity {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_main);
      if (savedInstanceState == null) {
          getFragmentManager().beginTransaction()
                .add(R.id.container, new SummaryFragment(),"SummaryFragment")
                .commit();
      }

      loadBrandSound();

     if (!isNetworkAvailable())
          showToast(getString(R.string.no_network));
  }

  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
      // Inflate the menu; this adds items to the action bar if it is   present.
      getMenuInflater().inflate(R.menu.menu_main, menu);
      return true;
  }

  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
      // Handle action bar item clicks here. The action bar will
      // automatically handle clicks on the Home/Up button, so long
      // as you specify a parent activity in AndroidManifest.xml.
      int id = item.getItemId();

      //noinspection SimplifiableIfStatement
      if (id == R.id.action_settings) {
          return true;
      }

      return super.onOptionsItemSelected(item);
  }
}

and here is my simple test case:

import android.test.ActivityInstrumentationTestCase2;
import android.widget.Button;
import android.widget.EditText;

import org.mockito.Mockito;

public class ApplicationTest extends ActivityInstrumentationTestCase2<MainActivity> {

    MainActivity mActivity;
    private Button goBtn;
    private EditText et_query;
    private RecyclerListAdapter mAdapter;

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

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

        setActivityInitialTouchMode(false);

        mActivity = getActivity();

        goBtn=(Button)mActivity.findViewById(
                R.id.btn_go);
        et_query = (EditText)mActivity.findViewById(R.id.et_query);
    }

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

    public void testPreconditions() {
        assertTrue(mActivity.isNetworkAvailable());
        isLayoutValid();
    }

    public void isLayoutValid(){
        assertNotNull(goBtn);
    }

    public void testSomething(){
        Mockito.spy(MainActivity.class);
    }

}

why is it failing on the spy line ? here is the log:

    Running tests
Test running started
org.mockito.exceptions.base.MockitoException:
Cannot mock/spy class java.lang.Class
Mockito cannot mock/spy following:
- final classes
- anonymous classes
- primitive types
at mypackage.ApplicationTest.testSomething(ApplicationTest.java:65)
at java.lang.reflect.Method.invokeNative(Native Method)
at android.test.InstrumentationTestCase.runMethod(InstrumentationTestCase.java:214)
at android.test.InstrumentationTestCase.runTest(InstrumentationTestCase.java:199)
at android.test.ActivityInstrumentationTestCase2.runTest(ActivityInstrumentationTestCase2.java:192)
at android.test.AndroidTestRunner.runTest(AndroidTestRunner.java:191)
at android.test.AndroidTestRunner.runTest(AndroidTestRunner.java:176)
at android.test.InstrumentationTestRunner.onStart(InstrumentationTestRunner.java:554)
at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:1729)

Finish

Upvotes: 5

Views: 13357

Answers (3)

Andrew Panasiuk
Andrew Panasiuk

Reputation: 656

There are few points in the question.

Firstly, what's happening when we call Mockito.spy(Consts.class): Mockito tries to proxy an object that you pass into spy method. In this case it is an instance of java.lang.Class that is returned by Consts.class statement. But Mockito cannot proxy an instance of final class which java.lang.Class is. See its code public final class Class

Then, for mocking/spying final classes or static methods you can use PowerMock tool. But it's not suitable for instrumentation tests. See this answer.

So, we should somehow redesign our code. The first option is to redesign the code we are testing. The second - is to use Robolectric and change our test to unit one.

Upvotes: 0

j2emanue
j2emanue

Reputation: 62549

For any one else having an issue specifically in Android ...i was not including the dex maker jar so that mockito can run with dex files the right way. Add this to your dependencies in gradle:

    androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.0'
androidTestCompile 'com.google.dexmaker:dexmaker:1.0'

Upvotes: 1

Jeff Bowman
Jeff Bowman

Reputation: 95704

Unlike Mockito.mock(Class<T> clazz), Mockito.spy(T instance) takes an instance. Both, however, return an instance. You're looking to replace static fields, not instance methods.

Mockito cannot mock static members or final classes, because it works predominantly by creating anonymous proxy implementations or subclasses. Subclasses can't override static methods or extend final classes, so the Java compiler takes a shortcut that skips any Mockito-provided implementations. (This is especially true of static final constants, which are inlined at compile time.)

Though PowerMockito can rewrite system-under-test code to mock final and static method calls, it doesn't help with fields. An additional level of indirection may be a better solution.

public class MyApplication extends Application {
  public boolean isUrlValid(String url) {
    return isUrlValid(url, Consts.SCHEME, Consts.TAG_JSON);
  }

  static boolean isUrlValid(String url, String scheme, String jsonTag) {
    // ...
  }
}

public class MyApplicationTest {
  @Test public void urlValidShouldWork() {
    assertTrue(MyApplication.isUrlValid(VALID_URL, "fakescheme", "faketag");
  }
}

As an alternative, make Consts operate in terms of accessor methods (getScheme), not fields (scheme). Tools like ProGuard can usually inline simple method calls, so it shouldn't be any slower or more verbose in production, and you'll have more opportunity to substitute implementations in tests. For more techniques to insert/inject mocked implementations in tests, see this SO question.

Upvotes: 3

Related Questions