Docs 菜单
Docs 主页
/ /
Atlas Device SDKs
/ /

测试 - Java SDK

在此页面上

  • 集成测试
  • 应用程序上下文
  • Looper 线程
  • 在异步调用完成时延迟测试执行
  • 测试后台
  • 测试 Atlas 集群
  • 完整示例
  • 单元测试
  • 完整示例

您可以使用单元测试或集成测试来测试应用程序。单元测试只评估应用程序代码中编写的逻辑。集成测试评估应用程序的逻辑、数据库查询和写入,以及对应用程序后端的调用(如果有的话)。单元测试在使用 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
}

活动对象 和变更通知等 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() {
@Override
public void onSuccess(@NonNull Realm realm) {
Log.v("EXAMPLE", "Successfully opened a realm.");
// read and write to realm here via transactions
testLatch.countDown();
realm.executeTransaction(new Realm.Transaction() {
@Override
public void execute(@NonNull Realm realm) {
realm.createObjectFromJson(Frog.class,
"{ name: \"Doctor Cucumber\", age: 1, species: \"bullfrog\", owner: \"Wirt\", _id: 0 }");
}
});
realm.close();
}
@Override
public void onError(@NonNull 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. 实例化计数为 1 的 CountDownLatch

  2. 运行测试需要等待的异步逻辑后,调用 CountDownLatch 实例的 countDown() 方法。

  3. 需要等待异步逻辑时,请添加处理 InterruptedExceptiontry / catch 区块。在该区块中,调用该 CountDownLatch 实例的 await() 方法。

  4. 将超时间隔和单位传递给 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() {
@Override
public void onSuccess(@NonNull Realm realm) {
Log.v("EXAMPLE", "Successfully opened a realm.");
// read and write to realm here via transactions
testLatch.countDown();
realm.executeTransaction(new Realm.Transaction() {
@Override
public void execute(@NonNull Realm realm) {
realm.createObjectFromJson(Frog.class,
"{ name: \"Doctor Cucumber\", age: 1, species: \"bullfrog\", owner: \"Wirt\", _id: 0 }");
}
});
realm.close();
}
@Override
public void onError(@NonNull 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 后端的应用程序不应出于测试目的连接到生产后端,原因如下:

  • 出于安全和保护隐私的考虑,应始终将测试用户和生产用户分开

  • 测试通常需要一个干净的初始状态,因此测试很可能包含一个删除了所有用户或大量数据的设置或删除方法

您可以使用环境分别管理测试和生产应用。

使用 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 {
@Test
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() {
@Override
public void onSuccess(@NonNull Realm realm) {
Log.v("EXAMPLE", "Successfully opened a realm.");
// read and write to realm here via transactions
testLatch.countDown();
realm.executeTransaction(new Realm.Transaction() {
@Override
public void execute(@NonNull Realm realm) {
realm.createObjectFromJson(Frog.class,
"{ name: \"Doctor Cucumber\", age: 1, species: \"bullfrog\", owner: \"Wirt\", _id: 0 }");
}
});
realm.close();
}
@Override
public void onError(@NonNull 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 {
@Test
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 文档示例应用程序”,了解本地和实时后台集成测试 SDK 的示例。

要对使用 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 的每个单元测试类:

@RunWith(RobolectricTestRunner.class)
@Config(sdk = 28)
@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*", "jdk.internal.reflect.*", "androidx.*"})
@SuppressStaticInitializationFor("io.realm.internal.Util")
@PrepareForTest({Realm.class, RealmConfiguration.class, RealmQuery.class, RealmResults.class, RealmCore.class, RealmLog.class})
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [28])
@PowerMockIgnore(
"org.mockito.*",
"org.robolectric.*",
"android.*",
"jdk.internal.reflect.*",
"androidx.*"
)
@SuppressStaticInitializationFor("io.realm.internal.Util")
@PrepareForTest(
Realm::class,
RealmConfiguration::class,
RealmQuery::class,
RealmResults::class,
RealmCore::class,
RealmLog::class
)

然后,在测试类中全局快速启动 Powermock:

// bootstrap powermock
@Rule
public PowerMockRule rule = new PowerMockRule();
// bootstrap powermock
@Rule
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;
@Override
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>() {
@Override
protected String doInBackground(Void... voids) {
String info = "";
info += complexQuery();
return info;
}
@Override
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));
}
@Override
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;
@RunWith(RobolectricTestRunner.class)
@Config(sdk = 28)
@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*", "jdk.internal.reflect.*", "androidx.*"})
@SuppressStaticInitializationFor("io.realm.internal.Util")
@PrepareForTest({Realm.class, RealmConfiguration.class, RealmQuery.class, RealmResults.class, RealmCore.class, RealmLog.class})
public class TestTest {
// bootstrap powermock
@Rule
public PowerMockRule rule = new PowerMockRule();
// mocked realm SDK components for tests
private Realm mockRealm;
private RealmResults<Cat> cats;
@Before
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;
}
@Test
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();
}
@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);
}
}
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
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [28])
@PowerMockIgnore(
"org.mockito.*",
"org.robolectric.*",
"android.*",
"jdk.internal.reflect.*",
"androidx.*"
)
@SuppressStaticInitializationFor("io.realm.internal.Util")
@PrepareForTest(
Realm::class,
RealmConfiguration::class,
RealmQuery::class,
RealmResults::class,
RealmCore::class,
RealmLog::class
)
class TestTest {
// bootstrap powermock
@Rule
var rule = PowerMockRule()
// mocked realm SDK components for tests
private var mockRealm: Realm? = null
private var cats: RealmResults<Cat>? = null
@Before
@Throws(Exception::class)
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
}
@Test
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>? {
@Suppress("UNCHECKED_CAST")
return PowerMockito.mock(RealmQuery::class.java) as RealmQuery<T>
}
private fun <T : RealmObject?> mockRealmResults(): RealmResults<T>? {
@Suppress("UNCHECKED_CAST")
return PowerMockito.mock(RealmResults::class.java) as RealmResults<T>
}
}

提示

另请参阅:

请参阅“单元测试示例应用程序”,了解对使用 Realm 的应用程序进行单元测试的示例。

后退

调试