App Actions 测试库

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 测试库依赖项

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

        allprojects {
            repositories {
                
                google()
            }
        }
    
  2. 在 app 模块的 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. 使参数映射中的 name 和 key 属性与 BII 中的参数匹配。例如,exercisePlan.forExercise.nameGET_EXERCISE_PLAN 参数的文档匹配。

  3. 使用 Android Context 参数(从 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 对象。

执行断言

断言 App Actions 测试库的推荐方式是

  1. 断言 AppActionsFulfillmentResult 的执行类型。它必须是 FulfillmentType.INTENTFulfillmentType.UNFULFILLED,以便测试应用在收到意外 BII 请求时的行为。
  2. 执行有两种类型:INTENTDEEPLINK 执行。
    • 通常,开发者可以通过查看 shortcuts.xml 中通过触发库来执行的 intent 标签来区分 INTENTDEEPLINK 执行。
    • 如果在 intent 标签下有 url-template 标签,则表示 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,并断言深层链接 url(通过 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 日志消息以获取警告或错误级别消息。您还可以提高日志记录级别以捕获库的更多输出。

限制

以下是 App Actions 测试库当前的限制

  • AATL 不测试自然语言理解 (NLU) 或语音转文本 (STT) 功能。
  • 当测试位于默认 app 模块以外的模块中时,AATL 不起作用。
  • AATL 仅兼容 Android 7.0 “牛轧糖”(API 级别 24)及更高版本。