앱 작업 테스트 라이브러리

앱 작업 테스트 라이브러리(AATL)는 개발자가 앱 작업 처리를 프로그래매틱 방식으로 테스트할 수 있는 기능을 제공하여 일반적으로 실제 음성 쿼리 또는 앱 작업 테스트 도구를 사용하여 실행할 테스트를 자동화합니다.

라이브러리는 shortcut.xml 구성이 올바르고 설명된 Android 인텐트 호출이 성공하는지 확인하는 데 도움이 됩니다. 앱 작업 테스트 라이브러리는 Google 어시스턴트 인텐트와 매개변수를 어설션하고 Android 활동을 인스턴스화하는 데 사용할 수 있는 Android 딥 링크 또는 Android 인텐트로 변환하여 지정된 Google 어시스턴트 인텐트와 매개변수를 처리할 수 있도록 앱 기능을 테스트하는 메커니즘을 제공합니다.

테스트는 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” />를 포함하는지 확인합니다.
    • <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 결과에 따라 활동을 실행하는 방법을 수정해야 합니다. 다음은 Espresso에서 ActivityScenario 메서드를 사용하는 예입니다.

    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.name은 GET_EXERCISE_PLAN의 매개변수에 관한 문서와 일치합니다.

  3. Android 컨텍스트 매개변수(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));
          }
        
      
  4. API의 fulfill 메서드를 실행하고 AppActionsFulfillmentResult 객체를 가져옵니다.

어설션 실행

앱 작업 테스트 라이브러리를 어설션하는 데 권장하는 방법은 다음과 같습니다.

  1. AppActionsFulfillmentResult의 처리 유형을 어설션합니다. 예상치 못한 BII 요청이 있는 경우 앱이 어떻게 동작하는지 테스트하려면 처리 유형이 FulfillmentType.INTENT 또는 FulfillmentType.UNFULFILLED여야 합니다.
  2. 처리의 두 가지 유형은 INTENT 처리와 DEEPLINK 처리입니다.
    • 일반적으로 개발자는 라이브러리를 트리거하여 처리 중인 shortcuts.xml의 인텐트 태그를 보고 INTENT 처리와 DEEPLINK 처리를 구분할 수 있습니다.
    • 인텐트 태그 아래에 URL 템플릿 태그가 있으면 이는 DEEPLINK가 이 인텐트를 처리함을 나타냅니다.
    • 결과 인텐트의 getData() 메서드가 null이 아닌 객체를 반환하는 경우도 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. INTENT와 DEEPLINK 모두 결과 인텐트를 사용하여 선택한 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 스튜디오 Logcat 창에서 AATL 로그 메시지를 찾아 경고 또는 오류 수준 메시지를 확인할 수 있습니다. 로깅 수준을 높여 라이브러리에서 더 많은 출력을 캡처할 수도 있습니다.

제한사항

앱 작업 테스트 라이브러리의 제한사항은 현재 다음과 같습니다.

  • AATL은 자연어 이해(NLU) 또는 음성 텍스트 변환(STT) 기능을 테스트하지 않습니다.
  • 테스트가 기본 앱 모듈이 아닌 다른 모듈에 있으면 AATL이 작동하지 않습니다.
  • AATL은 Android 7.0 'Nougat'(API 수준 24) 이상의 버전과만 호환됩니다.