与应用有关的 Action 测试库

与应用有关的 Action 测试库 (AATL) 提供多项功能,可让开发者以编程方式测试与应用有关的 Action 执行方式,从而自动执行通常使用实际语音查询或与应用有关的 Action 测试工具完成的测试。

该库有助于确保 shortcut.xml 配置正确无误且成功调用所述的 Android intent。与应用有关的 Action 测试库提供一种机制,可测试您的应用执行给定 Google 助理 intent 和参数的能力,方法是将这些 intent 和参数转换为可被断言并用于实例化 Android activity 的 Android 深层链接或 Android intent。

在 Android 环境中,测试形式为 Robolectric 单元测试或插桩测试。这样一来,开发者便可以通过模拟实际应用行为来全面测试其应用。如需测试 BII、自定义 intent 或深层链接执行方式,开发者可以使用任意插桩测试框架(UI Automator、Espresso、JUnit4、Appium、Detox、Calabash)。

如果应用支持多种语言,开发者可以验证应用的功能在不同的语言区域中是否正常运行。

运作方式

如需在应用的测试环境中集成与应用有关的 Action 测试库,开发者应在应用的 app 模块中创建新的或更新现有的 Robolectric 测试或插桩测试。

测试代码包含以下部分:

  • 在通用设置方法或单个测试用例中初始化库实例。
  • 每个单个测试都会调用库实例的 fulfill 方法,以生成 intent 创建结果。
  • 然后,开发者断言深层链接或触发应用执行方式,并针对应用状态运行自定义验证。

设置要求

为了使用测试库,您必须先调整一些初始应用配置,然后才能将测试添加到应用。

配置

如需使用与应用有关的 Action 测试库,请确保按如下方式配置您的应用:

  • 安装 Android Gradle 插件 (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> 元素内

添加与应用有关的 Action 测试库依赖项

  1. 将 Google 仓库添加到 settings.gradle 中的项目制品库列表中:

        allprojects {
            repositories {
                
                google()
            }
        }
    
  2. 在应用模块 build.gradle 文件中,添加 AATL 依赖项:

        androidTestImplementation 'com.google.assistant.appactions:testing:1.0.0'
    

    请务必使用您所下载的库的版本号。

创建集成测试

  1. 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…")))
        
  2. 参数映射中的名称和键属性应与 BII 中的参数相匹配。例如,exercisePlan.forExercise.nameGET_EXERCISE_PLAN 中参数的文档匹配。

  3. 使用 Android 上下文参数(从 ApplicationProviderInstrumentationRegistry 获取)实例化 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));
          }
        
      
  4. 执行 API 的 fulfill 方法并获取 AppActionsFulfillmentResult 对象。

执行断言

断言与应用有关的 Action 测试库的推荐方法是:

  1. 断言 AppActionsFulfillmentResult 的执行方式类型。它必须是 FulfillmentType.INTENTFulfillmentType.UNFULFILLED,才能测试应用在出现意外 BII 请求时的行为。
  2. 执行方式有两种:INTENTDEEPLINK 执行方式。
    • 通常,如需区分 INTENTDEEPLINK 执行方式,开发者只需查看 shortcuts.xml 中通过触发库实现的 intent 标记。
    • 如果 intent 标记下具有网址模板标记,则表示 DEEPLINK 会执行此 intent。
    • 如果结果 intent 的 getData() 方法返回非 null 对象,也表示 DEEPLINK 执行方式。同样,如果 getData 返回 null,则表示它是 INTENT 执行方式。
  3. 对于 INTENT 情形,将 AppActionsFulfillmentResult 转换为 AppActionsIntentFulfillmentResult,然后通过调用 getIntent 方法提取 Android intent,并执行以下其中一项操作:
    • 断言 Android intent 的各个字段。
    • 断言通过 intent.getData.getHost 方法访问的 intent 的 URI。
  4. 对于 DEEPLINK 情形,将 AppActionsFulfillmentResult 转换为 AppActionsIntentFulfillmentResult(与上面的 INTENT 情形相同),通过调用 getIntent 方法提取 Android intent,并断言深层链接网址(通过 intent.getData.getHost 访问)。
  5. 对于 INTENTDEEPLINK,您可以使用生成的 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 日志消息,以获取警告或错误级别的消息。您还可以提高日志记录级别,从库中截取更多输出。

限制

以下是与应用有关的 Action 测试库的当前限制:

  • AATL 不会测试自然语言理解 (NLU) 或语音转文字 (STT) 功能。
  • 如果测试位于默认应用模块以外的模块中,则 AATL 不起作用。
  • AATL 仅与 Android 7.0“Nougat”(API 级别 24)及更高版本兼容。