测试 - Java SDK
您可以使用单元测试或集成测试来测试应用程序。单元测试只评估应用程序代码中编写的逻辑。集成测试评估应用程序的逻辑、数据库查询和写入,以及对应用程序后端的调用(如果有的话)。单元测试在使用 JVM 的开发机器上运行,而集成测试则在物理或模拟 Android 设备上运行。您可以使用 Android 内置的仪器测试,通过与 Realm 或应用程序后端的实际实例通信来运行集成测试。
Android 在 Android 项目中使用特定的文件路径和文件夹名称进行单元测试和仪器化测试:
测试类型 | 路径 |
---|---|
单元测试 | /app/src/test |
仪器测试 | /app/src/androidTest |
由于 SDK 通过 Android Native 使用 C++ 代码进行数据存储,因此单元测试要求完全模拟与 Realm 的交互。对于需要与数据库进行大量交互的逻辑,最好进行集成测试。
集成测试
本节介绍如何对使用 Realm SDK 的应用程序开展集成测试。它涵盖测试环境中的以下概念:
获取应用程序上下文
在
Looper
线程上执行逻辑如何在异步方法调用完成时延迟测试执行
使用“同步”或后台应用程序的应用程序也需要(此处未涉及):
用于测试的独立应用后端,拥有独立的用户账户和数据
包含仅测试数据的单独 Atlas 集群
应用程序上下文
要初始化 SDK,您需要提供应用程序或活动 上下文 。默认,这在 Android 集成测试中不可用。但是,您可以使用 Android 的内置测试 ActivityScenario 类来启动测试中的活动。您可以使用应用程序中的任何活动,也可以创建一个仅用于测试的空活动。使用活动类作为参数调用ActivityScenario.launch()
以启动模拟活动。
接下来,使用 ActivityScenario.onActivity()
方法在模拟活动的主线程上运行 Lambda。在该 Lambda 中,应调用 Realm.init()
函数以将活动作为参数来初始化 SDK。此外,应该保存传递给 Lambda(活动的新建实例)的参数以供将来使用。
由于 onActivity()
方法在不同的线程上运行,因此在初始设置完成之前,您应该阻塞测试进一步执行。
以下示例使用 ActivityScenario
、空测试活动和 CountDownLatch
来演示如何设置可以测试 Realm 应用程序的环境:
AtomicReference<Activity> testActivity = new AtomicReference<Activity>(); ActivityScenario<BasicActivity> scenario = ActivityScenario.launch(BasicActivity.class); // create a latch to force blocking for an async call to initialize realm CountDownLatch setupLatch = new CountDownLatch(1); scenario.onActivity(activity -> { Realm.init(activity); testActivity.set(activity); setupLatch.countDown(); // unblock the latch await }); // block until we have an activity to run tests on try { Assert.assertTrue(setupLatch.await(1, TimeUnit.SECONDS)); } catch (InterruptedException e) { Log.e("EXAMPLE", e.getMessage()); }
var testActivity: Activity? = null val scenario: ActivityScenario<BasicActivity>? = ActivityScenario.launch(BasicActivity::class.java) // create a latch to force blocking for an async call to initialize realm val setupLatch = CountDownLatch(1) scenario?.onActivity{ activity: BasicActivity -> Realm.init(activity) testActivity = activity setupLatch.countDown() // unblock the latch await }
Looper 线程
活动对象 和变更通知等 Realm 功能仅适用于 Looper 线程。配置有Looper
对象的线程通过由Looper
协调的消息循环传递事件。 测试函数通常没有Looper
对象,而在测试中配置一个 对象可能非常容易出错。
相反,您可以使用 Activity.runOnUiThread()测试活动中的方法,以便在已配置Looper
的线程上执行逻辑。 如延迟部分所述,将Activity.runOnUiThread()
与CountDownLatch
结合使用,以防止测试在逻辑执行之前完成并退出。 在runOnUiThread()
调用中,您可以与 SDK 进行交互,就像通常在应用程序代码中一样:
testActivity.get().runOnUiThread(() -> { // instantiate an app connection String appID = YOUR_APP_ID; // replace this with your test application App ID App app = new App(new AppConfiguration.Builder(appID).build()); // authenticate a user Credentials credentials = Credentials.anonymous(); app.loginAsync(credentials, it -> { if (it.isSuccess()) { Log.v("EXAMPLE", "Successfully authenticated."); // open a synced realm SyncConfiguration config = new SyncConfiguration.Builder( app.currentUser(), getRandomPartition()) // replace this with a valid partition .allowQueriesOnUiThread(true) .allowWritesOnUiThread(true) .build(); Realm.getInstanceAsync(config, new Realm.Callback() { public void onSuccess( Realm realm) { Log.v("EXAMPLE", "Successfully opened a realm."); // read and write to realm here via transactions testLatch.countDown(); realm.executeTransaction(new Realm.Transaction() { public void execute( Realm realm) { realm.createObjectFromJson(Frog.class, "{ name: \"Doctor Cucumber\", age: 1, species: \"bullfrog\", owner: \"Wirt\", _id: 0 }"); } }); realm.close(); } public void onError( Throwable exception) { Log.e("EXAMPLE", "Failed to open the realm: " + exception.getLocalizedMessage()); } }); } else { Log.e("EXAMPLE", "Failed login: " + it.getError().getErrorMessage()); } }); });
testActivity?.runOnUiThread { // instantiate an app connection val appID: String = YOUR_APP_ID // replace this with your App ID val app = App(AppConfiguration.Builder(appID).build()) // authenticate a user val credentials = Credentials.anonymous() app.loginAsync(credentials) { if (it.isSuccess) { Log.v("EXAMPLE", "Successfully authenticated.") // open a synced realm val config = SyncConfiguration.Builder( app.currentUser(), getRandomPartition() // replace this with a valid partition ).allowQueriesOnUiThread(true) .allowWritesOnUiThread(true) .build() Realm.getInstanceAsync(config, object : Realm.Callback() { override fun onSuccess(realm: Realm) { Log.v("EXAMPLE", "Successfully opened a realm.") // read and write to realm here via transactions realm.executeTransaction { realm.createObjectFromJson( Frog::class.java, "{ name: \"Doctor Cucumber\", age: 1, species: \"bullfrog\", owner: \"Wirt\", _id:0 }" ) } testLatch.countDown() realm.close() } override fun onError(exception: Throwable) { Log.e("EXAMPLE", "Failed to open the realm: " + exception.localizedMessage) } }) } else { Log.e("EXAMPLE", "Failed login: " + it.error.errorMessage) } } }
在异步调用完成时延迟测试执行
由于 SDK 对数据库查询、身份验证和函数调用等常见操作使用异步调用,因此测试需要一种等待这些异步调用完成的方法。 否则,测试将在异步(或多线程)调用运行之前退出。 此示例使用 Java 的内置 CountDownLatch 。请按照以下步骤在自己的测试中使用CountDownLatch
:
实例化计数为 1 的
CountDownLatch
。运行测试需要等待的异步逻辑后,调用
CountDownLatch
实例的countDown()
方法。需要等待异步逻辑时,请添加处理
InterruptedException
的try
/catch
区块。在该区块中,调用该CountDownLatch
实例的await()
方法。将超时间隔和单位传递给
await()
,并将调用封装在Assert.assertTrue()
断言中。如果逻辑花费的时间太长,则await()
调用会超时,返回 false 并导致测试失败。
以下示例演示了如何使用 CountDownLatch
等待身份验证并在单独的线程上异步打开 Realm:
CountDownLatch testLatch = new CountDownLatch(1); testActivity.get().runOnUiThread(() -> { // instantiate an app connection String appID = YOUR_APP_ID; // replace this with your test application App ID App app = new App(new AppConfiguration.Builder(appID).build()); // authenticate a user Credentials credentials = Credentials.anonymous(); app.loginAsync(credentials, it -> { if (it.isSuccess()) { Log.v("EXAMPLE", "Successfully authenticated."); // open a synced realm SyncConfiguration config = new SyncConfiguration.Builder( app.currentUser(), getRandomPartition()) // replace this with a valid partition .allowQueriesOnUiThread(true) .allowWritesOnUiThread(true) .build(); Realm.getInstanceAsync(config, new Realm.Callback() { public void onSuccess( Realm realm) { Log.v("EXAMPLE", "Successfully opened a realm."); // read and write to realm here via transactions testLatch.countDown(); realm.executeTransaction(new Realm.Transaction() { public void execute( Realm realm) { realm.createObjectFromJson(Frog.class, "{ name: \"Doctor Cucumber\", age: 1, species: \"bullfrog\", owner: \"Wirt\", _id: 0 }"); } }); realm.close(); } public void onError( Throwable exception) { Log.e("EXAMPLE", "Failed to open the realm: " + exception.getLocalizedMessage()); } }); } else { Log.e("EXAMPLE", "Failed login: " + it.getError().getErrorMessage()); } }); }); // block until the async calls in the test succeed or error out try { Assert.assertTrue(testLatch.await(5, TimeUnit.SECONDS)); } catch (InterruptedException e) { Log.e("EXAMPLE", e.getMessage()); }
val testLatch = CountDownLatch(1) testActivity?.runOnUiThread { // instantiate an app connection val appID: String = YOUR_APP_ID // replace this with your App ID val app = App(AppConfiguration.Builder(appID).build()) // authenticate a user val credentials = Credentials.anonymous() app.loginAsync(credentials) { if (it.isSuccess) { Log.v("EXAMPLE", "Successfully authenticated.") // open a synced realm val config = SyncConfiguration.Builder( app.currentUser(), getRandomPartition() // replace this with a valid partition ).allowQueriesOnUiThread(true) .allowWritesOnUiThread(true) .build() Realm.getInstanceAsync(config, object : Realm.Callback() { override fun onSuccess(realm: Realm) { Log.v("EXAMPLE", "Successfully opened a realm.") // read and write to realm here via transactions realm.executeTransaction { realm.createObjectFromJson( Frog::class.java, "{ name: \"Doctor Cucumber\", age: 1, species: \"bullfrog\", owner: \"Wirt\", _id:0 }" ) } testLatch.countDown() realm.close() } override fun onError(exception: Throwable) { Log.e("EXAMPLE", "Failed to open the realm: " + exception.localizedMessage) } }) } else { Log.e("EXAMPLE", "Failed login: " + it.error.errorMessage) } } } // block until the async calls in the test succeed or error out try { Assert.assertTrue(testLatch.await(5, TimeUnit.SECONDS)) } catch (e: InterruptedException) { Log.e("EXAMPLE", e.stackTraceToString()) }
测试后台
使用 App Services 后端的应用程序不应出于测试目的连接到生产后端,原因如下:
出于安全和保护隐私的考虑,应始终将测试用户和生产用户分开
测试通常需要一个干净的初始状态,因此测试很可能包含一个删除了所有用户或大量数据的设置或删除方法
您可以使用环境分别管理测试和生产应用。
测试 Atlas 集群
使用 Sync 或 MongoDB 查询的应用程序可以读取、写入、更新或删除存储在连接的 Atlas 集群中的数据。出于安全考虑,不应将生产数据和测试数据存储在同一个集群。此外,测试可能需要模式更改,然后才能在生产应用程序中妥善处理这些更改。因此,在测试应用程序时,应该使用单独的 Atlas 集群。
完整示例
以下示例显示了在集成测试中运行 Realm 的完整 Junit 工具的 androidTest
示例:
package com.mongodb.realm.examples.java; import android.app.Activity; import android.util.Log; import androidx.annotation.NonNull; import androidx.test.core.app.ActivityScenario; import com.mongodb.realm.examples.BasicActivity; import com.mongodb.realm.examples.model.kotlin.Frog; import org.junit.Assert; import org.junit.Test; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import io.realm.Realm; import io.realm.mongodb.App; import io.realm.mongodb.AppConfiguration; import io.realm.mongodb.Credentials; import io.realm.mongodb.sync.SyncConfiguration; import static com.mongodb.realm.examples.RealmTestKt.YOUR_APP_ID; import static com.mongodb.realm.examples.RealmTestKt.getRandomPartition; public class TestTest { public void testTesting() { AtomicReference<Activity> testActivity = new AtomicReference<Activity>(); ActivityScenario<BasicActivity> scenario = ActivityScenario.launch(BasicActivity.class); // create a latch to force blocking for an async call to initialize realm CountDownLatch setupLatch = new CountDownLatch(1); scenario.onActivity(activity -> { Realm.init(activity); testActivity.set(activity); setupLatch.countDown(); // unblock the latch await }); // block until we have an activity to run tests on try { Assert.assertTrue(setupLatch.await(1, TimeUnit.SECONDS)); } catch (InterruptedException e) { Log.e("EXAMPLE", e.getMessage()); } CountDownLatch testLatch = new CountDownLatch(1); testActivity.get().runOnUiThread(() -> { // instantiate an app connection String appID = YOUR_APP_ID; // replace this with your test application App ID App app = new App(new AppConfiguration.Builder(appID).build()); // authenticate a user Credentials credentials = Credentials.anonymous(); app.loginAsync(credentials, it -> { if (it.isSuccess()) { Log.v("EXAMPLE", "Successfully authenticated."); // open a synced realm SyncConfiguration config = new SyncConfiguration.Builder( app.currentUser(), getRandomPartition()) // replace this with a valid partition .allowQueriesOnUiThread(true) .allowWritesOnUiThread(true) .build(); Realm.getInstanceAsync(config, new Realm.Callback() { public void onSuccess( Realm realm) { Log.v("EXAMPLE", "Successfully opened a realm."); // read and write to realm here via transactions testLatch.countDown(); realm.executeTransaction(new Realm.Transaction() { public void execute( Realm realm) { realm.createObjectFromJson(Frog.class, "{ name: \"Doctor Cucumber\", age: 1, species: \"bullfrog\", owner: \"Wirt\", _id: 0 }"); } }); realm.close(); } public void onError( Throwable exception) { Log.e("EXAMPLE", "Failed to open the realm: " + exception.getLocalizedMessage()); } }); } else { Log.e("EXAMPLE", "Failed login: " + it.getError().getErrorMessage()); } }); }); // block until the async calls in the test succeed or error out try { Assert.assertTrue(testLatch.await(5, TimeUnit.SECONDS)); } catch (InterruptedException e) { Log.e("EXAMPLE", e.getMessage()); } } }
package com.mongodb.realm.examples.kotlin import android.app.Activity import android.util.Log import androidx.test.core.app.ActivityScenario import com.mongodb.realm.examples.BasicActivity import com.mongodb.realm.examples.YOUR_APP_ID import com.mongodb.realm.examples.getRandomPartition import com.mongodb.realm.examples.model.kotlin.Frog import io.realm.Realm import io.realm.mongodb.App import io.realm.mongodb.AppConfiguration import io.realm.mongodb.Credentials import io.realm.mongodb.sync.SyncConfiguration import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import org.junit.Assert import org.junit.Test class TestTest { fun testTesting() { var testActivity: Activity? = null val scenario: ActivityScenario<BasicActivity>? = ActivityScenario.launch(BasicActivity::class.java) // create a latch to force blocking for an async call to initialize realm val setupLatch = CountDownLatch(1) scenario?.onActivity{ activity: BasicActivity -> Realm.init(activity) testActivity = activity setupLatch.countDown() // unblock the latch await } // block until we have an activity to run tests on try { Assert.assertTrue(setupLatch.await(1, TimeUnit.SECONDS)) } catch (e: InterruptedException) { Log.e("EXAMPLE", e.stackTraceToString()) } val testLatch = CountDownLatch(1) testActivity?.runOnUiThread { // instantiate an app connection val appID: String = YOUR_APP_ID // replace this with your App ID val app = App(AppConfiguration.Builder(appID).build()) // authenticate a user val credentials = Credentials.anonymous() app.loginAsync(credentials) { if (it.isSuccess) { Log.v("EXAMPLE", "Successfully authenticated.") // open a synced realm val config = SyncConfiguration.Builder( app.currentUser(), getRandomPartition() // replace this with a valid partition ).allowQueriesOnUiThread(true) .allowWritesOnUiThread(true) .build() Realm.getInstanceAsync(config, object : Realm.Callback() { override fun onSuccess(realm: Realm) { Log.v("EXAMPLE", "Successfully opened a realm.") // read and write to realm here via transactions realm.executeTransaction { realm.createObjectFromJson( Frog::class.java, "{ name: \"Doctor Cucumber\", age: 1, species: \"bullfrog\", owner: \"Wirt\", _id:0 }" ) } testLatch.countDown() realm.close() } override fun onError(exception: Throwable) { Log.e("EXAMPLE", "Failed to open the realm: " + exception.localizedMessage) } }) } else { Log.e("EXAMPLE", "Failed login: " + it.error.errorMessage) } } } // block until the async calls in the test succeed or error out try { Assert.assertTrue(testLatch.await(5, TimeUnit.SECONDS)) } catch (e: InterruptedException) { Log.e("EXAMPLE", e.stackTraceToString()) } } }
单元测试
要对使用 Realm 的 Realm 应用程序进行单元测试,必须模拟 Realm(以及应用程序后端,如果使用了后端)。使用以下库模拟 SDK 功能:
如需在 Android 项目中将这些库用于单元测试,请在应用程序 build.gradle
文件的 dependencies
块中添加以下内容:
testImplementation "org.robolectric:robolectric:4.1" testImplementation "org.mockito:mockito-core:3.3.3" testImplementation "org.powermock:powermock-module-junit4:2.0.9" testImplementation "org.powermock:powermock-module-junit4-rule:2.0.9" testImplementation "org.powermock:powermock-api-mockito2:2.0.9" testImplementation "org.powermock:powermock-classloading-xstream:2.0.9"
注意
版本兼容性
在单元测试中模拟 SDK 需要 Robolectric、Mockito 和 Powermock,因为 SDK 使用 Android Native C++ 方法调用来与 Realm 交互。由于覆盖这些方法调用所需的框架可能很复杂,因此应使用上面列出的版本,以确保模拟成功。最近的一些版本更新(特别是 Robolectric 版本 4.2+)可能会破坏使用 SDK 进行单元测试的编译。
要将单元测试配置为通过 SDK 使用 Robolectric、PowerMock 和 Mockito,请将以下注解添加到模拟 SDK 的每个单元测试类:
然后,在测试类中全局快速启动 Powermock:
// bootstrap powermock public PowerMockRule rule = new PowerMockRule();
// bootstrap powermock var rule = PowerMockRule()
接下来,模拟 SDK 中可能会查询本地 C++ 代码的组件,这样我们就不会受到测试环境的限制:
// set up realm SDK components to be mocked. The order of these matters mockStatic(RealmCore.class); mockStatic(RealmLog.class); mockStatic(Realm.class); mockStatic(RealmConfiguration.class); Realm.init(RuntimeEnvironment.application); // boilerplate to mock realm components -- this prevents us from hitting any // native code doNothing().when(RealmCore.class); RealmCore.loadLibrary(any(Context.class));
// set up realm SDK components to be mocked. The order of these matters PowerMockito.mockStatic(RealmCore::class.java) PowerMockito.mockStatic(RealmLog::class.java) PowerMockito.mockStatic(Realm::class.java) PowerMockito.mockStatic(RealmConfiguration::class.java) Realm.init(RuntimeEnvironment.application) PowerMockito.doNothing().`when`(RealmCore::class.java) RealmCore.loadLibrary(ArgumentMatchers.any(Context::class.java))
完成模拟所需的设置后,就可以开始模拟组件并为测试连接行为。您还可以配置 PowerMockito,使其在实例化某一类型的新对象时返回特定对象,这样,即使在应用程序中引用默认 Realm 的代码也不会破坏您的测试:
// create the mocked realm final Realm mockRealm = mock(Realm.class); final RealmConfiguration mockRealmConfig = mock(RealmConfiguration.class); // use this mock realm config for all new realm configurations whenNew(RealmConfiguration.class).withAnyArguments().thenReturn(mockRealmConfig); // use this mock realm for all new default realms when(Realm.getDefaultInstance()).thenReturn(mockRealm);
// create the mocked realm val mockRealm = PowerMockito.mock(Realm::class.java) val mockRealmConfig = PowerMockito.mock( RealmConfiguration::class.java ) // use this mock realm config for all new realm configurations PowerMockito.whenNew(RealmConfiguration::class.java).withAnyArguments() .thenReturn(mockRealmConfig) // use this mock realm for all new default realms PowerMockito.`when`(Realm.getDefaultInstance()).thenReturn(mockRealm)
模拟 Realm 后,您必须为测试用例配置数据。请参阅下面的完整示例,了解如何在单元测试中提供测试数据的一些示例。
完整示例
以下示例显示了在单元测试中模拟 Realm 的完整 JUnit test
示例。此示例测试执行一些基本 Realm 操作的活动。在单元测试期间启动该活动时,测试会使用模拟来模拟这些操作:
package com.mongodb.realm.examples.java; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.os.AsyncTask; import android.util.Log; import android.widget.LinearLayout; import android.widget.TextView; import com.mongodb.realm.examples.R; import com.mongodb.realm.examples.model.java.Cat; import io.realm.Realm; import io.realm.RealmResults; public class UnitTestActivity extends AppCompatActivity { public static final String TAG = UnitTestActivity.class.getName(); private LinearLayout rootLayout = null; private Realm realm; protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Realm.init(getApplicationContext()); setContentView(R.layout.activity_unit_test); rootLayout = findViewById(R.id.container); rootLayout.removeAllViews(); // open the default Realm for the UI thread. realm = Realm.getDefaultInstance(); // clean up from previous run cleanUp(); // small operation that is ok to run on the main thread basicCRUD(realm); // more complex operations can be executed on another thread. AsyncTask<Void, Void, String> foo = new AsyncTask<Void, Void, String>() { protected String doInBackground(Void... voids) { String info = ""; info += complexQuery(); return info; } protected void onPostExecute(String result) { showStatus(result); } }; foo.execute(); findViewById(R.id.clean_up).setOnClickListener(view -> { view.setEnabled(false); Log.d("TAG", "clean up"); cleanUp(); view.setEnabled(true); }); } private void cleanUp() { // delete all cats realm.executeTransaction(r -> r.delete(Cat.class)); } public void onDestroy() { super.onDestroy(); realm.close(); // remember to close realm when done. } private void showStatus(String txt) { Log.i(TAG, txt); TextView tv = new TextView(this); tv.setText(txt); rootLayout.addView(tv); } private void basicCRUD(Realm realm) { showStatus("Perform basic Create/Read/Update/Delete (CRUD) operations..."); // all writes must be wrapped in a transaction to facilitate safe multi threading realm.executeTransaction(r -> { // add a cat Cat cat = r.createObject(Cat.class); cat.setName("John Young"); }); // find the first cat (no query conditions) and read a field final Cat cat = realm.where(Cat.class).findFirst(); showStatus(cat.getName()); // update cat in a transaction realm.executeTransaction(r -> { cat.setName("John Senior"); }); showStatus(cat.getName()); // add two more cats realm.executeTransaction(r -> { Cat jane = r.createObject(Cat.class); jane.setName("Jane"); Cat doug = r.createObject(Cat.class); doug.setName("Robert"); }); RealmResults<Cat> cats = realm.where(Cat.class).findAll(); showStatus(String.format("Found %s cats", cats.size())); for (Cat p : cats) { showStatus("Found " + p.getName()); } } private String complexQuery() { String status = "\n\nPerforming complex Query operation..."; Realm realm = Realm.getDefaultInstance(); status += "\nNumber of cats in the DB: " + realm.where(Cat.class).count(); // find all cats where name begins with "J". RealmResults<Cat> results = realm.where(Cat.class) .beginsWith("name", "J") .findAll(); status += "\nNumber of cats whose name begins with 'J': " + results.size(); realm.close(); return status; } }
import android.content.Context; import com.mongodb.realm.examples.java.UnitTestActivity; import com.mongodb.realm.examples.model.java.Cat; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.powermock.core.classloader.annotations.PowerMockIgnore; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.core.classloader.annotations.SuppressStaticInitializationFor; import org.powermock.modules.junit4.rule.PowerMockRule; import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; import java.util.Arrays; import java.util.List; import io.realm.Realm; import io.realm.RealmConfiguration; import io.realm.RealmObject; import io.realm.RealmQuery; import io.realm.RealmResults; import io.realm.internal.RealmCore; import io.realm.log.RealmLog; import com.mongodb.realm.examples.R; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.doCallRealMethod; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.powermock.api.mockito.PowerMockito.doNothing; import static org.powermock.api.mockito.PowerMockito.mock; import static org.powermock.api.mockito.PowerMockito.mockStatic; import static org.powermock.api.mockito.PowerMockito.when; import static org.powermock.api.mockito.PowerMockito.whenNew; public class TestTest { // bootstrap powermock public PowerMockRule rule = new PowerMockRule(); // mocked realm SDK components for tests private Realm mockRealm; private RealmResults<Cat> cats; public void setup() throws Exception { // set up realm SDK components to be mocked. The order of these matters mockStatic(RealmCore.class); mockStatic(RealmLog.class); mockStatic(Realm.class); mockStatic(RealmConfiguration.class); Realm.init(RuntimeEnvironment.application); // boilerplate to mock realm components -- this prevents us from hitting any // native code doNothing().when(RealmCore.class); RealmCore.loadLibrary(any(Context.class)); // create the mocked realm final Realm mockRealm = mock(Realm.class); final RealmConfiguration mockRealmConfig = mock(RealmConfiguration.class); // use this mock realm config for all new realm configurations whenNew(RealmConfiguration.class).withAnyArguments().thenReturn(mockRealmConfig); // use this mock realm for all new default realms when(Realm.getDefaultInstance()).thenReturn(mockRealm); // any time we ask Realm to create a Cat, return a new instance. when(mockRealm.createObject(Cat.class)).thenReturn(new Cat()); // set up test data Cat p1 = new Cat(); p1.setName("Enoch"); Cat p2 = new Cat(); p2.setName("Quincy Endicott"); Cat p3 = new Cat(); p3.setName("Sara"); Cat p4 = new Cat(); p4.setName("Jimmy Brown"); List<Cat> catList = Arrays.asList(p1, p2, p3, p4); // create a mocked RealmQuery RealmQuery<Cat> catQuery = mockRealmQuery(); // when the RealmQuery performs findFirst, return the first record in the list. when(catQuery.findFirst()).thenReturn(catList.get(0)); // when the where clause is called on the Realm, return the mock query. when(mockRealm.where(Cat.class)).thenReturn(catQuery); // when the RealmQuery is filtered on any string and any integer, return the query when(catQuery.equalTo(anyString(), anyInt())).thenReturn(catQuery); // when a between query is performed with any string as the field and any int as the // value, then return the catQuery itself when(catQuery.between(anyString(), anyInt(), anyInt())).thenReturn(catQuery); // When a beginsWith clause is performed with any string field and any string value // return the same cat query when(catQuery.beginsWith(anyString(), anyString())).thenReturn(catQuery); // RealmResults is final, must mock static and also place this in the PrepareForTest // annotation array. mockStatic(RealmResults.class); // create a mock RealmResults RealmResults<Cat> cats = mockRealmResults(); // 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, any time // anyone asks for the RealmResults Iterator, give them a functioning iterator from the // ArrayList of Cats we created above. This will allow the loop to execute. when(cats.iterator()).thenReturn(catList.iterator()); // Return the size of the mock list. when(cats.size()).thenReturn(catList.size()); // when we ask Realm for all of the Cat instances, return the mock RealmResults when(mockRealm.where(Cat.class).findAll()).thenReturn(cats); // when we ask the RealmQuery for all of the Cat objects, return the mock RealmResults when(catQuery.findAll()).thenReturn(cats); this.mockRealm = mockRealm; this.cats = cats; } public void shouldBeAbleToAccessActivityAndVerifyRealmInteractions() { doCallRealMethod().when(mockRealm) .executeTransaction(any(Realm.Transaction.class)); // create test activity -- onCreate method calls methods that // query/write to realm UnitTestActivity activity = Robolectric .buildActivity(UnitTestActivity.class) .create() .start() .resume() .visible() .get(); // click the clean up button activity.findViewById(R.id.clean_up).performClick(); // verify that we queried for Cat instances five times in this run // (2 in basicCrud(), 2 in complexQuery() and 1 in the button click) verify(mockRealm, times(5)).where(Cat.class); // verify that the delete method was called. We also call delete at // the start of the activity to ensure we start with a clean db. verify(mockRealm, times(2)).delete(Cat.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(); } private <T extends RealmObject> RealmQuery<T> mockRealmQuery() { return mock(RealmQuery.class); } private <T extends RealmObject> RealmResults<T> mockRealmResults() { return mock(RealmResults.class); } }
package com.mongodb.realm.examples.kotlin import android.os.AsyncTask import android.os.Bundle import android.util.Log import android.view.View import android.widget.LinearLayout import android.widget.TextView import androidx.appcompat.app.AppCompatActivity import com.mongodb.realm.examples.R import com.mongodb.realm.examples.model.java.Cat import io.realm.Realm class UnitTestActivity : AppCompatActivity() { private var rootLayout: LinearLayout? = null private var realm: Realm? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Realm.init(applicationContext) setContentView(R.layout.activity_unit_test) rootLayout = findViewById(R.id.container) rootLayout!!.removeAllViews() // open the default Realm for the UI thread. realm = Realm.getDefaultInstance() // clean up from previous run cleanUp() // small operation that is ok to run on the main thread basicCRUD(realm) // more complex operations can be executed on another thread. val foo: AsyncTask<Void?, Void?, String> = object : AsyncTask<Void?, Void?, String>() { protected override fun doInBackground(vararg params: Void?): String? { var info = "" info += complexQuery() return info } override fun onPostExecute(result: String) { showStatus(result) } } foo.execute() findViewById<View>(R.id.clean_up).setOnClickListener { view: View -> view.isEnabled = false Log.d("TAG", "clean up") cleanUp() view.isEnabled = true } } private fun cleanUp() { // delete all cats realm!!.executeTransaction { r: Realm -> r.delete(Cat::class.java) } } public override fun onDestroy() { super.onDestroy() realm!!.close() // remember to close realm when done. } private fun showStatus(txt: String) { Log.i(TAG, txt) val tv = TextView(this) tv.text = txt rootLayout!!.addView(tv) } private fun basicCRUD(realm: Realm?) { showStatus("Perform basic Create/Read/Update/Delete (CRUD) operations...") // all writes must be wrapped in a transaction to facilitate safe multi threading realm!!.executeTransaction { r: Realm -> // add a cat val cat = r.createObject(Cat::class.java) cat.name = "John Young" } // find the first cat (no query conditions) and read a field val cat = realm.where(Cat::class.java).findFirst() showStatus(cat!!.name) // update cat in a transaction realm.executeTransaction { r: Realm? -> cat.name = "John Senior" } showStatus(cat.name) // add two more cats realm.executeTransaction { r: Realm -> val jane = r.createObject(Cat::class.java) jane.name = "Jane" val doug = r.createObject(Cat::class.java) doug.name = "Robert" } val cats = realm.where(Cat::class.java).findAll() showStatus(String.format("Found %s cats", cats.size)) for (p in cats) { showStatus("Found " + p.name) } } private fun complexQuery(): String { var status = "\n\nPerforming complex Query operation..." val realm = Realm.getDefaultInstance() status += """ Number of cats in the DB: ${realm.where(Cat::class.java).count()} """.trimIndent() // find all cats where name begins with "J". val results = realm.where(Cat::class.java) .beginsWith("name", "J") .findAll() status += """ Number of cats whose name begins with 'J': ${results.size} """.trimIndent() realm.close() return status } companion object { val TAG = UnitTestActivity::class.java.name } }
import android.content.Context import android.view.View import com.mongodb.realm.examples.R import com.mongodb.realm.examples.kotlin.UnitTestActivity import com.mongodb.realm.examples.model.java.Cat import io.realm.Realm import io.realm.RealmConfiguration import io.realm.RealmObject import io.realm.RealmQuery import io.realm.RealmResults import io.realm.internal.RealmCore import io.realm.log.RealmLog import java.lang.Exception import java.util.* import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers import org.mockito.Mockito import org.powermock.api.mockito.PowerMockito import org.powermock.core.classloader.annotations.PowerMockIgnore import org.powermock.core.classloader.annotations.PrepareForTest import org.powermock.core.classloader.annotations.SuppressStaticInitializationFor import org.powermock.modules.junit4.rule.PowerMockRule import org.robolectric.Robolectric import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment import org.robolectric.annotation.Config class TestTest { // bootstrap powermock var rule = PowerMockRule() // mocked realm SDK components for tests private var mockRealm: Realm? = null private var cats: RealmResults<Cat>? = null fun setup() { // set up realm SDK components to be mocked. The order of these matters PowerMockito.mockStatic(RealmCore::class.java) PowerMockito.mockStatic(RealmLog::class.java) PowerMockito.mockStatic(Realm::class.java) PowerMockito.mockStatic(RealmConfiguration::class.java) Realm.init(RuntimeEnvironment.application) PowerMockito.doNothing().`when`(RealmCore::class.java) RealmCore.loadLibrary(ArgumentMatchers.any(Context::class.java)) // create the mocked realm val mockRealm = PowerMockito.mock(Realm::class.java) val mockRealmConfig = PowerMockito.mock( RealmConfiguration::class.java ) // use this mock realm config for all new realm configurations PowerMockito.whenNew(RealmConfiguration::class.java).withAnyArguments() .thenReturn(mockRealmConfig) // use this mock realm for all new default realms PowerMockito.`when`(Realm.getDefaultInstance()).thenReturn(mockRealm) // any time we ask Realm to create a Cat, return a new instance. PowerMockito.`when`(mockRealm.createObject(Cat::class.java)).thenReturn(Cat()) // set up test data val p1 = Cat() p1.name = "Enoch" val p2 = Cat() p2.name = "Quincy Endicott" val p3 = Cat() p3.name = "Sara" val p4 = Cat() p4.name = "Jimmy Brown" val catList = Arrays.asList(p1, p2, p3, p4) // create a mocked RealmQuery val catQuery = mockRealmQuery<Cat>() // when the RealmQuery performs findFirst, return the first record in the list. PowerMockito.`when`(catQuery!!.findFirst()).thenReturn(catList[0]) // when the where clause is called on the Realm, return the mock query. PowerMockito.`when`(mockRealm.where(Cat::class.java)).thenReturn(catQuery) // when the RealmQuery is filtered on any string and any integer, return the query PowerMockito.`when`( catQuery.equalTo( ArgumentMatchers.anyString(), ArgumentMatchers.anyInt() ) ).thenReturn(catQuery) // when a between query is performed with any string as the field and any int as the // value, then return the catQuery itself PowerMockito.`when`( catQuery.between( ArgumentMatchers.anyString(), ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt() ) ).thenReturn(catQuery) // When a beginsWith clause is performed with any string field and any string value // return the same cat query PowerMockito.`when`( catQuery.beginsWith( ArgumentMatchers.anyString(), ArgumentMatchers.anyString() ) ).thenReturn(catQuery) // RealmResults is final, must mock static and also place this in the PrepareForTest // annotation array. PowerMockito.mockStatic(RealmResults::class.java) // create a mock RealmResults val cats = mockRealmResults<Cat>() // 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, any time // anyone asks for the RealmResults Iterator, give them a functioning iterator from the // ArrayList of Cats we created above. This will allow the loop to execute. PowerMockito.`when`<Iterator<Cat>>(cats!!.iterator()).thenReturn(catList.iterator()) // Return the size of the mock list. PowerMockito.`when`(cats.size).thenReturn(catList.size) // when we ask Realm for all of the Cat instances, return the mock RealmResults PowerMockito.`when`(mockRealm.where(Cat::class.java).findAll()).thenReturn(cats) // when we ask the RealmQuery for all of the Cat objects, return the mock RealmResults PowerMockito.`when`(catQuery.findAll()).thenReturn(cats) this.mockRealm = mockRealm this.cats = cats } fun shouldBeAbleToAccessActivityAndVerifyRealmInteractions() { Mockito.doCallRealMethod().`when`(mockRealm)!! .executeTransaction(ArgumentMatchers.any(Realm.Transaction::class.java)) // create test activity -- onCreate method calls methods that // query/write to realm val activity = Robolectric .buildActivity(UnitTestActivity::class.java) .create() .start() .resume() .visible() .get() // click the clean up button activity.findViewById<View>(R.id.clean_up).performClick() // verify that we queried for Cat instances five times in this run // (2 in basicCrud(), 2 in complexQuery() and 1 in the button click) Mockito.verify(mockRealm, Mockito.times(5))!!.where(Cat::class.java) // verify that the delete method was called. We also call delete at // the start of the activity to ensure we start with a clean db. Mockito.verify(mockRealm, Mockito.times(2))!!.delete(Cat::class.java) // 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 Mockito.verify(mockRealm, Mockito.times(2))!!.close() } private fun <T : RealmObject?> mockRealmQuery(): RealmQuery<T>? { return PowerMockito.mock(RealmQuery::class.java) as RealmQuery<T> } private fun <T : RealmObject?> mockRealmResults(): RealmResults<T>? { return PowerMockito.mock(RealmResults::class.java) as RealmResults<T> } }