Search code examples
androidunit-testingmockitorealmpowermockito

Realm Unit Testing


I am trying to unit test Realm and its interactions but things are not going too well. I have included all dependencies and keep getting vague failures, below is my code for the Helper class which is a wrapper over Realm.

Questions

  1. Is this the correct way of testing Realm?

  2. How can I test data that is in the app's sandbox, can that data only be tested by UI/Instrumentation tests?

  3. I am getting an error currently (below) and before I was getting a "Powermock zero args constructor doesn't exist"

GitHub repo

Below is the current code I have for my Unit test:

@RunWith(PowerMockRunner.class)
@PowerMockRunnerDelegate(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21, application = CustomApplicationTest.class)
@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*", "javax.crypto.","java.security.*"})
@SuppressStaticInitializationFor("io.realm.internal.Util")
@PrepareForTest({Realm.class, RealmConfiguration.class,
    RealmQuery.class, RealmResults.class, RealmCore.class, RealmLog.class})
public class DatabaseHelperTest {

@Rule
public PowerMockRule rule = new PowerMockRule();

private DatabaseHelper dB;

private Realm realmMock;


@Before
public void setUp() throws Exception {
    MockitoAnnotations.initMocks(this);

    mockStatic(Realm.class);
    mockStatic(RealmConfiguration.class);
    mockStatic(RealmCore.class);
    mock(DatabaseHelper.class);

    final Realm mockRealm = PowerMockito.mock(Realm.class);
    realmMock = mockRealm;

    final RealmConfiguration mockRealmConfig = PowerMockito.mock(RealmConfiguration.class);

    doNothing().when(RealmCore.class);
    RealmCore.loadLibrary(any(Context.class));

    whenNew(RealmConfiguration.class).withAnyArguments().thenReturn(mockRealmConfig);
    when(Realm.getInstance(any(RealmConfiguration.class))).thenReturn(mockRealm);
    when(Realm.getDefaultInstance()).thenReturn(mockRealm);

    when(Realm.getDefaultInstance()).thenReturn(realmMock);

    when(realmMock.createObject(Person.class)).thenReturn(new Person());

    Person person = new Person();
    person.setId("2");
    person.setName("Jerry");
    person.setAge("25");

    Person person2 = new Person();
    person.setId("3");
    person.setName("Tom");
    person.setAge("22");

    List<Person> personsList = new ArrayList<>();
    personsList.add(person);
    personsList.add(person2);

    RealmQuery<Person> personRealmQuery = mockRealmQuery();
    when(realmMock.where(Person.class)).thenReturn(personRealmQuery);

    RealmResults<Person> personRealmResults = mockRealmResults();
    when(realmMock.where(Person.class).findAll()).thenReturn(personRealmResults);
    when(personRealmResults.iterator()).thenReturn(personsList.iterator());
    when(personRealmResults.size()).thenReturn(personsList.size());

    when(realmMock.copyFromRealm(personRealmResults)).thenReturn(personsList);

    realmMock = mockRealm;
    dB = new DatabaseHelper(realmMock);
}


@Test
public void insertingPerson(){
    doCallRealMethod().when(realmMock).executeTransaction(any(Realm.Transaction.class));

    Person person = mock(Person.class);
    when(realmMock.createObject(Person.class)).thenReturn(person);

    dB.putPersonData();

    verify(realmMock, times(1)).createObject(Person.class);
    verify(person, times(1)).setId(anyString());
}


@Test
public void testExistingData(){
    List<Person> personList = dB.getPersonList();
    //NPE if checking person object properties i.e name, id. Only list size is available why?
    Assert.assertEquals(2, personList.size());

}

@SuppressWarnings("unchecked")
private <T extends RealmObject> RealmQuery<T> mockRealmQuery() {
    return mock(RealmQuery.class);
}

@SuppressWarnings("unchecked")
private <T extends RealmObject> RealmResults<T> mockRealmResults() {
    return mock(RealmResults.class);
}

Error:

org.mockito.exceptions.misusing.NotAMockException: 
Argument passed to verify() is of type Realm$$EnhancerByMockitoWithCGLIB$$317bc746 and is not a mock!
Make sure you place the parenthesis correctly!
See the examples of correct verifications:
verify(mock).someMethod();
verify(mock, times(10)).someMethod();
verify(mock, atLeastOnce()).someMethod();

at com.appstronomy.realmunittesting.db.DatabaseHelperTest.insertingPerson(DatabaseHelperTest.java:133)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

Solution

  • Is this the correct way of testing Realm?

    How about following the official tests. While instrumentation tests seem easy, 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();
        }
    

    OLDER ANSWER

    https://medium.com/@q2ad/android-testing-realm-2dc1e1c94ee1 has a great proposal: do not mock Realm, but use a temporary instance instead. Original proposition with dependency injection: Use

    RealmConfiguration testConfig = 
       new RealmConfiguration.Builder().
          inMemory().
          name("test-realm").build();
    
    Realm testRealm = Realm.getInstance(testConfig);
    

    If dependency injection is not possible, you could use

    Realm.setDefaultConfiguration(testConfig);
    

    instead, which sets the Realm returned by Realm.getDefaultInstance().


    EDIT: If you receive a java.lang.IllegalStateException, remember to call Realm.init(InstrumentationRegistry.getTargetContext()) beforehand, and put the files inside the android-test directory. (that is: use an instrumentation test, not a unit test).