Frank
Frank

Reputation: 12298

Realm Unit Test on Android

Trying to unit test a class that does some calls to Realm (0.87.4), the test setup fails with

java.lang.NoClassDefFoundError: rx/Observable$OnSubscribe
at io.realm.RealmConfiguration$Builder.<init>(RealmConfiguration.java:279)
at org.testapp.db.MyClassTest.setUp(MyClassTest.java:34)
... 
Caused by: java.lang.ClassNotFoundException: rx.Observable$OnSubscribe
at java.net.URLClassLoader$1.run(URLClassLoader.java:366)

My test class starts with:

@RunWith(MockitoJUnitRunner.class)
public class MyClassTest extends TestCase {

@Rule
public TemporaryFolder testFolder = new TemporaryFolder();

Realm realm;

@Before
public void setUp() throws Exception {
    File tempFolder = testFolder.newFolder("realmdata");
    RealmConfiguration config = new RealmConfiguration.Builder(tempFolder).build();

    realm = Realm.getInstance(config);
}
...

My gradle has:

testCompile 'junit:junit:4.12'
testCompile "org.mockito:mockito-core:1.10.19"
testCompile "org.robolectric:robolectric:3.0"
compile 'io.realm:realm-android:0.87.4'

How to solve this?

=== EDIT 1 ===

I added to my gradle:

testCompile 'io.reactivex:rxjava:1.1.0'

and

android {
  // ...
  testOptions { 
    unitTests.returnDefaultValues = true
  }
}

the new error is

java.lang.UnsatisfiedLinkError: no realm-jni in java.library.path
at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1886)
at java.lang.Runtime.loadLibrary0(Runtime.java:849)
at java.lang.System.loadLibrary(System.java:1088)
at io.realm.internal.RealmCore.loadLibrary(RealmCore.java:117)

Upvotes: 5

Views: 6214

Answers (3)

serv-inc
serv-inc

Reputation: 38267

The official tests show how to do this. While instrumentation tests seem easy (as you found out), unit test are quite involved:

@RunWith(PowerMockRunner.class)
@PowerMockRunnerDelegate(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21)
@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*"})
@SuppressStaticInitializationFor("io.realm.internal.Util")
@PrepareForTest({Realm.class, RealmConfiguration.class, RealmQuery.class, RealmResults.class, RealmCore.class, RealmLog.class})
public class ExampleActivityTest {
    // Robolectric, Using Power Mock https://github.com/robolectric/robolectric/wiki/Using-PowerMock

    @Rule
    public PowerMockRule rule = new PowerMockRule();

    private Realm mockRealm;
    private RealmResults<Person> people;

    @Before
    public void setup() throws Exception {

        // Setup Realm to be mocked. The order of these matters
        mockStatic(RealmCore.class);
        mockStatic(RealmLog.class);
        mockStatic(Realm.class);
        mockStatic(RealmConfiguration.class);
        Realm.init(RuntimeEnvironment.application);

        // Create the mock
        final Realm mockRealm = mock(Realm.class);
        final RealmConfiguration mockRealmConfig = mock(RealmConfiguration.class);

        // TODO: Better solution would be just mock the RealmConfiguration.Builder class. But it seems there is some
        // problems for powermock to mock it (static inner class). We just mock the RealmCore.loadLibrary(Context) which
        // will be called by RealmConfiguration.Builder's constructor.
        doNothing().when(RealmCore.class);
        RealmCore.loadLibrary(any(Context.class));


        // TODO: Mock the RealmConfiguration's constructor. If the RealmConfiguration.Builder.build can be mocked, this
        // is not necessary anymore.
        whenNew(RealmConfiguration.class).withAnyArguments().thenReturn(mockRealmConfig);

        // Anytime getInstance is called with any configuration, then return the mockRealm
        when(Realm.getDefaultInstance()).thenReturn(mockRealm);

        // Anytime we ask Realm to create a Person, return a new instance.
        when(mockRealm.createObject(Person.class)).thenReturn(new Person());

        // Set up some naive stubs
        Person p1 = new Person();
        p1.setAge(14);
        p1.setName("John Young");

        Person p2 = new Person();
        p2.setAge(89);
        p2.setName("John Senior");

        Person p3 = new Person();
        p3.setAge(27);
        p3.setName("Jane");

        Person p4 = new Person();
        p4.setAge(42);
        p4.setName("Robert");

        List<Person> personList = Arrays.asList(p1, p2, p3, p4);

        // Create a mock RealmQuery
        RealmQuery<Person> personQuery = mockRealmQuery();

        // When the RealmQuery performs findFirst, return the first record in the list.
        when(personQuery.findFirst()).thenReturn(personList.get(0));

        // When the where clause is called on the Realm, return the mock query.
        when(mockRealm.where(Person.class)).thenReturn(personQuery);

        // When the RealmQuery is filtered on any string and any integer, return the person query
        when(personQuery.equalTo(anyString(), anyInt())).thenReturn(personQuery);

        // RealmResults is final, must mock static and also place this in the PrepareForTest annotation array.
        mockStatic(RealmResults.class);

        // Create a mock RealmResults
        RealmResults<Person> people = mockRealmResults();

        // When we ask Realm for all of the Person instances, return the mock RealmResults
        when(mockRealm.where(Person.class).findAll()).thenReturn(people);

        // When a between query is performed with any string as the field and any int as the
        // value, then return the personQuery itself
        when(personQuery.between(anyString(), anyInt(), anyInt())).thenReturn(personQuery);

        // When a beginsWith clause is performed with any string field and any string value
        // return the same person query
        when(personQuery.beginsWith(anyString(), anyString())).thenReturn(personQuery);

        // When we ask the RealmQuery for all of the Person objects, return the mock RealmResults
        when(personQuery.findAll()).thenReturn(people);


        // The for(...) loop in Java needs an iterator, so we're giving it one that has items,
        // since the mock RealmResults does not provide an implementation. Therefore, anytime
        // anyone asks for the RealmResults Iterator, give them a functioning iterator from the
        // ArrayList of Persons we created above. This will allow the loop to execute.
        when(people.iterator()).thenReturn(personList.iterator());

        // Return the size of the mock list.
        when(people.size()).thenReturn(personList.size());

        this.mockRealm = mockRealm;
        this.people = people;
    }


    @Test
    public void shouldBeAbleToAccessActivityAndVerifyRealmInteractions() {
        doCallRealMethod().when(mockRealm).executeTransaction(Mockito.any(Realm.Transaction.class));

        // Create activity
        ExampleActivity activity = Robolectric.buildActivity(ExampleActivity.class).create().start().resume().visible().get();

        assertThat(activity.getTitle().toString(), is("Unit Test Example"));

        // Verify that two Realm.getInstance() calls took place.
        verifyStatic(times(2));
        Realm.getDefaultInstance();

        // verify that we have four begin and commit transaction calls
        // Do not verify partial mock invocation count: https://github.com/jayway/powermock/issues/649
        //verify(mockRealm, times(4)).executeTransaction(Mockito.any(Realm.Transaction.class));

        // Click the clean up button
        activity.findViewById(R.id.clean_up).performClick();

        // Verify that begin and commit transaction were called (been called a total of 5 times now)
        // Do not verify partial mock invocation count: https://github.com/jayway/powermock/issues/649
        //verify(mockRealm, times(5)).executeTransaction(Mockito.any(Realm.Transaction.class));

        // Verify that we queried for Person instances five times in this run (2 in basicCrud(),
        // 2 in complexQuery() and 1 in the button click)
        verify(mockRealm, times(5)).where(Person.class);

        // Verify that the delete method was called. Delete is also called in the start of the
        // activity to ensure we start with a clean db.
        verify(mockRealm, times(2)).delete(Person.class);

        // Call the destroy method so we can verify that the .close() method was called (below)
        activity.onDestroy();

        // Verify that the realm got closed 2 separate times. Once in the AsyncTask, once
        // in onDestroy
        verify(mockRealm, times(2)).close();
    }

Upvotes: 0

Frank
Frank

Reputation: 12298

Unit tests are hard or impossible when using Realm in the class that you are testing (thanks Dmitry for mentioning). What I can do is run the tests as instrumental tests (thanks Dmitry, Christian).

And that is quite easy, I won't have to change anything to the test methods...

A. Move the test class into an "androidTest" folder, instead of "test". (Since Android Studio 1.1 you should put your Unit tests in /src/test and Android Instrumentation Tests in /src/androidTest)

B. Add the dependencies for instrumental tests in the gradle build file, use "androidTest" because they're instrumental:

androidTestCompile 'junit:junit:4.12'
androidTestCompile 'io.reactivex:rxjava:1.1.0'
androidTestCompile 'com.android.support.test:runner:0.4.1'
androidTestCompile 'com.android.support.test:rules:0.4.1'
androidTestCompile 'com.android.support:support-annotations:23.1.1'

C. In the test class, replace the runner at the top with AndroidJUnit4:

@RunWith(AndroidJUnit4.class)
public class MyClassTest extends TestCase {
...

Create an Android run configuration of type "Android Tests", run it and voila, it will test the same methods fine now, but on a device. Makes me very happy.

Upvotes: 8

Dmitry Zaytsev
Dmitry Zaytsev

Reputation: 23972

Since Realm 0.87 you also need to include RxJava to your dependencies:

compile 'io.reactivex:rxjava:1.1.0'

Upvotes: 3

Related Questions