应用操作测试库

应用操作测试库 (AATL) 提供功能,使开发者能够以编程方式测试应用操作的实现,自动执行通常使用实际语音查询或应用操作测试工具进行的测试。

该库有助于确保 shortcut.xml 配置正确,并且描述的 Android 意图调用成功。应用操作测试库提供了一种机制来测试应用程序在给定 Google Assistant 意图和参数的情况下实现的能力,方法是将它们转换为 Android 深度链接或 Android 意图,这些意图可以断言并用于实例化 Android 活动。

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

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

工作原理

为了将应用操作测试库集成到应用程序的测试环境中,开发者应该在应用程序的 app 模块上创建新的或更新现有的 Robolectric 或仪器测试。

测试代码包含以下部分

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

设置要求

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

配置

要使用应用操作测试库,请确保您的应用程序配置如下

  • 安装 Android Gradle 插件 (AGP)
  • app 模块的 res/xml 文件夹中包含 shortcuts.xml 文件。
  • 确保 AndroidManifest.xml 在以下任一位置包含 <meta-data android:name="android.app.shortcuts" android:resource=”@xml/shortcuts” />
    • the <application> 标签
    • 启动器 <activity> 标签
  • <capability> 元素放在 shortcuts.xml 中的 <shortcuts> 元素内

添加应用操作测试库依赖项

  1. settings.gradle 中将 Google 存储库添加到项目存储库列表中

        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 中的参数匹配。例如,order.orderedItem.nameGET_ORDER 中参数的文档匹配。

  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 对象。

执行断言

建议断言应用操作测试库的方式是

  1. 断言 AppActionsFulfillmentResult 的实现类型。它必须是 FulfillmentType.INTENT,或者在测试应用程序在遇到意外 BII 请求时如何运行的情况下是 FulfillmentType.UNFULFILLED
  2. 实现有两种形式:INTENTDEEPLINK 实现。
    • 通常,开发者可以通过查看要通过触发库来实现的 shortcuts.xml 中的 intent 标签来区分 INTENTDEEPLINK 实现。
    • 如果 intent 标签下有 url-template 标签,则表明 DEEPLINK 实现此 intent。
    • 如果结果意图的 getData() 方法返回非空对象,则也表示 DEEPLINK 满足。类似地,如果 getData 返回 null,则意味着它是 INTENT 满足。
  3. 对于 INTENT 情况,将 AppActionsFulfillmentResult 转换为 AppActionsIntentFulfillmentResult,通过调用 getIntent 方法获取 Android 意图,然后执行以下操作之一
    • 断言 Android 意图的各个字段。
    • 断言通过 intent.getData.getHost 方法访问的意图的 uri。
  4. 对于 DEEPLINK 情况,将 AppActionsFulfillmentResult 转换为 AppActionsIntentFulfillmentResult(与上面的 INTENT 场景相同),通过调用 getIntent 方法获取 Android 意图,并断言深层链接 URL(通过 intent.getData.getHost 访问)。
  5. 对于 INTENTDEEPLINK,您可以使用生成的意图使用选择的 Android 测试框架启动活动。

国际化

如果您的 App 有多种语言环境,您可以配置测试以在测试中运行特定语言环境。或者,您可以直接更改语言环境

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.ORDER_MENU_ITEM"
          val intentParams = ImmutableMap.of("menuItem.name", "hamburguesa con queso")

          val result = aatl.fulfill(intentName, intentParams)
          assertThat(result.getFulfillmentType()).isEqualTo(FulfillmentType.INTENT)

          val intentResult = result as AppActionsFulfillmentIntentResult

          assertThat(intentResult.getIntent().getData().toString())
            .isEqualTo("myfoodapp://browse?food=food_hamburger")
        }
      }
      
    

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.ORDER_MENU_ITEM";
        ImmutableMap<String, String> intentParams = ImmutableMap.of("menuItem.name", "hamburguesa con queso");

        AppActionsFulfillmentResult result = aatl.fulfill(intentName, intentParams);
        assertThat(result.getFulfillmentType()).isEqualTo(FulfillmentType.INTENT);

        AppActionsFulfillmentIntentResult intentResult = (AppActionsFulfillmentIntentResult) result;

        assertThat(intentResult.getIntent().getData().toString())
          .isEqualTo("myfoodapp://browse?food=food_hamburger");
      }
      
    

故障排除

如果您的集成测试意外失败,您可以在 Android Studio logcat 窗口中查找 AATL 日志消息,以获取警告或错误级别消息。您也可以 提高日志记录级别 以从库中捕获更多输出。

限制

以下是 App Actions Test Library 的当前限制

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