Reputation: 12298
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
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
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
Reputation: 23972
Since Realm 0.87 you also need to include RxJava to your dependencies:
compile 'io.reactivex:rxjava:1.1.0'
Upvotes: 3