앱 작업 테스트 라이브러리

앱 작업 테스트 라이브러리(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.nameGET_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 처리를 나타냅니다. 반대로 getDatanull을 반환하면 이는 INTENT 처리입니다.
  3. INTENT 처리의 경우 AppActionsFulfillmentResultAppActionsIntentFulfillmentResult로 형변환하려면 getIntent 메서드를 호출하여 Android 인텐트를 가져오고 다음 중 하나를 실행합니다.
    • Android 인텐트의 개별 필드를 어설션합니다.
    • intent.getData.getHost 메서드를 통해 액세스하는 인텐트의 URI를 어설션합니다.
  4. DEEPLINK 처리의 경우 AppActionsFulfillmentResultAppActionsIntentFulfillmentResult로 형변환하려면(위의 INTENT 시나리오와 동일) getIntent 메서드를 호출하여 Android 인텐트를 가져오고 딥 링크 URL을 어설션합니다(intent.getData.getHost를 통해 액세스).
  5. INTENTDEEPLINK 모두 결과 인텐트를 사용하여 선택한 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) 이상의 버전과만 호환됩니다.