App Actions 测试库

App Actions 测试库 (AATL) 提供的功能使开发者能够以编程方式测试 App Action 履行,从而自动化通常使用实际语音查询或 App Actions 测试工具进行的测试。

该库有助于确保shortcut.xml配置正确,并且描述的 Android 意图调用成功。App Actions 测试库提供了一种机制来测试您的应用履行给定的 Google 助理意图和参数的能力,方法是将它们转换为可以断言并用于实例化 Android 活动的 Android 深层链接或 Android 意图。

测试以 Robolectric 单元测试或 Android 环境中的工具测试的形式执行。这允许开发者通过模拟实际应用行为来全面测试他们的应用。对于测试 BII、自定义意图或深层链接履行,可以使用任何工具测试框架(UI Automator、Espresso、JUnit4、Appium、Detox、Calabash)。

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

工作原理

要在应用的测试环境中集成 App Actions 测试库,开发者应在应用的app模块中创建新的 Robolectric 或工具测试或更新现有的测试。

测试代码包含以下部分

  • 在公共设置方法或单个测试用例中初始化库实例。
  • 每个单独的测试都会调用库实例的fulfill方法来生成意图创建结果。
  • 然后,开发者断言深层链接或触发应用履行,并在应用状态上运行自定义验证。

设置要求

为了使用测试库,在将测试添加到您的应用之前,需要进行一些初始的应用配置。

配置

要使用 App Actions 测试库,请确保您的应用配置如下:

  • 安装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>元素内

添加 App Actions 测试库依赖项

  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 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的履行类型。为了测试应用在意外 BII 请求时的行为,它必须是FulfillmentType.INTENTFulfillmentType.UNFULFILLED
  2. 履行有两种类型:INTENTDEEPLINK履行。
    • 通常,开发者可以通过查看他们正在通过触发库来履行的shortcuts.xml中的意图标签来区分INTENTDEEPLINK履行。
    • 如果意图标签下有 url-template 标签,则表示DEEPLINK履行了此意图。
    • 如果结果意图的getData()方法返回非空对象,这也表示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测试框架启动活动。

国际化

如果您的应用有多个语言环境,您可以配置测试以运行特定的被测语言环境。或者,您可以直接更改语言环境。

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)功能。
  • 当测试位于默认应用程序模块以外的模块中时,AATL不起作用。
  • AATL仅与Android 7.0“牛轧糖”(API级别24)及更高版本兼容。