App Actions 测试库 (AATL) 提供相关功能,使开发者能够以编程方式测试 App Actions 的执行,从而自动化原本需要使用实际语音查询或 App Actions 测试工具完成的测试。
该库有助于确保 shortcut.xml
配置正确无误,并且所描述的 Android intent 调用能够成功。App Actions 测试库提供了一种机制,用于通过将给定的 Google Assistant intent 和参数转换为 Android 深层链接或 Android intent 来测试应用执行这些 intent 和参数的能力,然后可以断言并用于实例化 Android activity。
测试以 Robolectric 单元测试或 Android 环境中的插桩测试形式进行。这使得开发者能够通过模拟实际应用行为来全面测试其应用。对于测试 BII、自定义 intent 或深层链接执行,可以使用任何插桩测试框架(UI Automator、Espresso、JUnit4、Appium、Detox、Calabash)。
如果应用是多语言的,开发者可以验证应用功能在不同语言区域设置中是否正常运行。
工作原理
要将 App Actions 测试库集成到应用的测试环境中,开发者应在应用的 app
模块上创建新的或更新现有的 Robolectric 或插桩测试。
测试代码包含以下部分
- 在通用设置方法或单独的测试用例中初始化库实例。
- 每个单独的测试都调用库实例的
fulfill
方法来产生 intent 创建结果。 - 然后,开发者断言深层链接或触发应用执行,并对应用状态运行自定义验证。
设置要求
要使用测试库,在将测试添加到您的应用之前,需要进行一些初始应用配置。
配置
要使用 App Actions 测试库,请确保您的应用按如下方式配置
- 安装 Android Gradle Plugin (AGP)
- 在
app
模块的res/xml
文件夹中包含一个shortcuts.xml
文件。 - 确保
AndroidManifest.xml
在以下任一标签下包含<meta-data android:name="android.app.shortcuts" android:resource=”@xml/shortcuts” />
- 的
<application>
标签 - 启动器
<activity>
标签
- 的
- 将
<capability>
元素放在shortcuts.xml
文件中的<shortcuts>
元素内
添加 App Actions 测试库依赖项
将 Google 仓库添加到
settings.gradle
中的项目仓库列表中allprojects { repositories { … google() } }
在 app 模块的
build.gradle
文件中,添加 AATL 依赖项androidTestImplementation 'com.google.assistant.appactions:testing:1.0.0'
请务必使用您下载的库的版本号。
创建集成测试
在
app/src/androidTest
下创建新测试。对于 Robolectric 测试,请在app/src/test
下创建。Kotlin
import android.content.Context import android.content.Intent import android.widget.TextView import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ActivityScenario import com.google.assistant.appactions.testing.aatl.AppActionsTestManager import com.google.assistant.appactions.testing.aatl.fulfillment.AppActionsFulfillmentIntentResult import com.google.assistant.appactions.testing.aatl.fulfillment.AppActionsFulfillmentResult import com.google.assistant.appactions.testing.aatl.fulfillment.FulfillmentType import com.google.common.collect.ImmutableMap import org.junit.Assert.assertEquals import org.junit.Before import org.junit.runner.RunWith import org.junit.Test import org.robolectric.RobolectricTestRunner … @Test fun IntentTestExample() { val intentParams = mapOf("feature" to "settings") val intentName = "actions.intent.OPEN_APP_FEATURE" val result = aatl.fulfill(intentName, intentParams) assertEquals(FulfillmentType.INTENT, result.getFulfillmentType()) val intentResult = result as AppActionsFulfillmentIntentResult val intent = intentResult.intent // Developer can choose to assert different relevant properties of the returned intent, such as the action, activity, package, scheme and so on assertEquals("youtube", intent.scheme) assertEquals("settings", intent.getStringExtra("featureParam")) assertEquals("actions.intent.OPEN_APP_FEATURE", intent.action) assertEquals("com.google.android.youtube/.MainActivity", intent.component.flattenToShortString()) assertEquals("com.google.myapp", intent.package) // Developers can choose to use returned Android Intent to launch and assess the activity. Below are examples for how it will look like for Robolectric and Espresso tests. // Please note that the below part is just a possible example of how Android tests are validating Activity functionality correctness for given Android Intent. // Robolectric example: val activity = Robolectric.buildActivity(MainActivity::class.java, intentResult.intent).create().resume().get() val title: TextView = activity.findViewById(R.id.startActivityTitle) assertEquals(title?.text?.toString(), "Launching…") }
Java
import android.content.Context; import android.content.Intent; import android.widget.TextView; import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ActivityScenario; import com.google.assistant.appactions.testing.aatl.AppActionsTestManager; import com.google.assistant.appactions.testing.aatl.fulfillment.AppActionsFulfillmentIntentResult; import com.google.assistant.appactions.testing.aatl.fulfillment.AppActionsFulfillmentResult; import com.google.assistant.appactions.testing.aatl.fulfillment.FulfillmentType; import com.google.common.collect.ImmutableMap; import org.junit.Assert.assertEquals; import org.junit.Before; import org.junit.runner.RunWith; import org.junit.Test; import org.robolectric.RobolectricTestRunner; ... @Test public void IntentTestExample() throws Exception { Map<String, String> intentParams = ImmutableMap.of("feature", "settings"); String intentName = "actions.intent.OPEN_APP_FEATURE"; AppActionsFulfillmentResult result = aatl.fulfill(intentName, intentParams); assertEquals(FulfillmentType.INTENT, result.getFulfillmentType()); AppActionsFulfillmentIntentResult intentResult = (AppActionsFulfillmentIntentResult) result; Intent intent = intentResult.getIntent(); // Developer can choose to assert different relevant properties of the returned intent, such as the action, activity, package, or scheme assertEquals("settings", intent.getStringExtra("featureParam")); assertEquals("actions.intent.OPEN_APP_FEATURE", intent.getAction()); assertEquals("com.google.android.youtube/.MainActivity", intent.getComponent().flattenToShortString()); assertEquals("com.google.myapp", intent.getPackage()); // Developers can choose to use returned Android Intent to launch and assess the activity. Below are examples for how it will look like for Robolectric and Espresso tests. // Please note that the below part is just a possible example of how Android tests are validating Activity functionality correctness for given Android Intent. // Robolectric example: MainActivity activity = Robolectric.buildActivity(MainActivity.class,intentResult.intent).create().resume().get(); TextView title: TextView = activity.findViewById(R.id.startActivityTitle) assertEquals(title?.getText()?.toString(), "Launching…") }
如果您使用的是 Espresso,则需要根据 AATL 结果修改启动 Activity 的方式。以下是使用
ActivityScenario
方法的 Espresso 示例Kotlin
ActivityScenario.launch<MainActivity>(intentResult.intent); Espresso.onView(ViewMatchers.withId(R.id.startActivityTitle)) .check(ViewAssertions.matches(ViewMatchers.withText("Launching…")))
Java
ActivityScenario.launch<MainActivity>(intentResult.intent); Espresso.onView(ViewMatchers.withId(R.id.startActivityTitle)) .check(ViewAssertions.matches(ViewMatchers.withText("Launching…")))
使参数映射中的 name 和 key 属性与 BII 中的参数匹配。例如,
exercisePlan.forExercise.name
与GET_EXERCISE_PLAN
参数的文档匹配。使用 Android Context 参数(从
ApplicationProvider
或InstrumentationRegistry
获取)实例化 API 实例- 单模块应用架构
Kotlin
private lateinit var aatl: AppActionsTestManager @Before fun init() { val appContext = ApplicationProvider.getApplicationContext() aatl = AppActionsTestManager(appContext) }
Java
private AppActionsTestManager aatl; @Before public void init() { Context appContext = ApplicationProvider.getApplicationContext(); aatl = new AppActionsTestManager(appContext); }
- 多模块应用架构
Kotlin
private lateinit var aatl: AppActionsTestManager @Before fun init() { val appContext = ApplicationProvider.getApplicationContext() val lookupPackages = listOf("com.myapp.mainapp", "com.myapp.resources") aatl = AppActionsTestManager(appContext, lookupPackages) }
Java
private AppActionsTestManager aatl; @Before public void init() throws Exception { Context appContext = ApplicationProvider.getApplicationContext(); List<String> lookupPackages = Arrays.asList("com.myapp.mainapp","com.myapp.resources"); aatl = new AppActionsTestManager(appContext, Optional.of(lookupPackages)); }
执行 API 的
fulfill
方法并获取AppActionsFulfillmentResult
对象。
执行断言
断言 App Actions 测试库的推荐方式是
- 断言
AppActionsFulfillmentResult
的执行类型。它必须是FulfillmentType.INTENT
或FulfillmentType.UNFULFILLED
,以便测试应用在收到意外 BII 请求时的行为。 - 执行有两种类型:
INTENT
和DEEPLINK
执行。- 通常,开发者可以通过查看
shortcuts.xml
中通过触发库来执行的 intent 标签来区分INTENT
和DEEPLINK
执行。 - 如果在 intent 标签下有 url-template 标签,则表示
DEEPLINK
执行此 intent。 - 如果结果 intent 的
getData()
方法返回非 null 对象,这也表示DEEPLINK
执行。同样,如果getData
返回null
,则表示它是INTENT
执行。
- 通常,开发者可以通过查看
- 对于
INTENT
情况,将AppActionsFulfillmentResult
强制转换为AppActionsIntentFulfillmentResult
,通过调用getIntent
方法获取 Android Intent,然后执行以下操作之一- 断言 Android Intent 的各个字段。
- 断言通过 intent.getData.getHost 方法访问的 intent 的 uri。
- 对于
DEEPLINK
情况,将AppActionsFulfillmentResult
强制转换为AppActionsIntentFulfillmentResult
(与上面的INTENT
场景相同),通过调用getIntent
方法获取 Android Intent,并断言深层链接 url(通过intent.getData.getHost
访问)。 - 对于
INTENT
和DEEPLINK
,您都可以使用结果 intent 通过选择的 Android 测试框架启动 Activity。
国际化
如果您的应用有多个语言区域设置,您可以配置测试以在特定的测试语言区域设置下运行。或者,您也可以直接更改语言区域设置
Kotlin
import android.content.res.Configuration import java.util.Locale ... val newLocale = Locale("es") val conf = context.resources.configuration conf = Configuration(conf) conf.setLocale(newLocale)
Java
Locale newLocale = new Locale("es"); Configuration conf = context.getResources().getConfiguration(); conf = new Configuration(conf); conf.setLocale(newLocale);
以下是为西班牙语 (ES) 语言区域设置配置的 AATL 测试示例
Kotlin
import com.google.common.truth.Truth.assertThat import org.junit.Assert.assertEquals import android.content.Context import android.content.res.Configuration import androidx.test.platform.app.InstrumentationRegistry import com.google.assistant.appactions.testing.aatl.AppActionsTestManager import com.google.assistant.appactions.testing.aatl.fulfillment.AppActionsFulfillmentIntentResult import com.google.assistant.appactions.testing.aatl.fulfillment.AppActionsFulfillmentResult import com.google.assistant.appactions.testing.aatl.fulfillment.FulfillmentType import com.google.common.collect.ImmutableMap import java.util.Locale import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) class ShortcutForDifferentLocaleTest { @Before fun setUp() { val context = InstrumentationRegistry.getInstrumentation().getContext() // change the device locale to 'es' val newLocale = Locale("es") val conf = context.resources.configuration conf = Configuration(conf) conf.setLocale(newLocale) val localizedContext = context.createConfigurationContext(conf) } @Test fun shortcutForDifferentLocale_succeeds() { val aatl = AppActionsTestManager(localizedContext) val intentName = "actions.intent.GET_EXERCISE_PLAN" val intentParams = ImmutableMap.of("exercisePlan.forExercise.name", "Running") val result = aatl.fulfill(intentName, intentParams) assertThat(result.getFulfillmentType()).isEqualTo(FulfillmentType.INTENT) val intentResult = result as AppActionsFulfillmentIntentResult assertThat(intentResult.getIntent().getData().toString()) .isEqualTo("myexercise://browse?plan=running_weekly") } }
Java
import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import android.content.Context; import android.content.res.Configuration; import androidx.test.platform.app.InstrumentationRegistry; import com.google.assistant.appactions.testing.aatl.AppActionsTestManager; import com.google.assistant.appactions.testing.aatl.fulfillment.AppActionsFulfillmentIntentResult; import com.google.assistant.appactions.testing.aatl.fulfillment.AppActionsFulfillmentResult; import com.google.assistant.appactions.testing.aatl.fulfillment.FulfillmentType; import com.google.common.collect.ImmutableMap; import java.util.Locale; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; @Test public void shortcutForDifferentLocale_succeeds() throws Exception { Context context = InstrumentationRegistry.getInstrumentation().getContext(); // change the device locale to 'es' Locale newLocale = new Locale("es"); Configuration conf = context.getResources().getConfiguration(); conf = new Configuration(conf); conf.setLocale(newLocale); Context localizedContext = context.createConfigurationContext(conf); AppActionsTestManager aatl = new AppActionsTestManager(localizedContext); String intentName = "actions.intent.GET_EXERCISE_PLAN"; ImmutableMap<String, String> intentParams = ImmutableMap.of("exercisePlan.forExercise.name", "Running"); AppActionsFulfillmentResult result = aatl.fulfill(intentName, intentParams); assertThat(result.getFulfillmentType()).isEqualTo(FulfillmentType.INTENT); AppActionsFulfillmentIntentResult intentResult = (AppActionsFulfillmentIntentResult) result; assertThat(intentResult.getIntent().getData().toString()) .isEqualTo("myexercise://browse?plan=running_weekly"); }
问题排查
如果您的集成测试意外失败,您可以在 Android Studio logcat 窗口中查找 AATL 日志消息以获取警告或错误级别消息。您还可以提高日志记录级别以捕获库的更多输出。
限制
以下是 App Actions 测试库当前的限制
- AATL 不测试自然语言理解 (NLU) 或语音转文本 (STT) 功能。
- 当测试位于默认 app 模块以外的模块中时,AATL 不起作用。
- AATL 仅兼容 Android 7.0 “牛轧糖”(API 级别 24)及更高版本。