앱 작업 테스트 라이브러리(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>
요소 내에 배치합니다.
앱 작업 테스트 라이브러리 종속 항목 추가
settings.gradle
의 프로젝트 저장소 목록에 Google 저장소를 추가합니다.allprojects { repositories { … google() } }
앱 모듈
build.gradle
파일에 AATL 종속 항목을 추가합니다.androidTestImplementation 'com.google.assistant.appactions:testing:1.0.0'
다운로드한 라이브러리의 버전 번호를 사용해야 합니다.
통합 테스트 만들기
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…")))
매개변수 매핑에 있는 이름 및 키 속성이 BII의 매개변수와 일치해야 합니다. 예를 들어
exercisePlan.forExercise.name
은GET_EXERCISE_PLAN
의 매개변수에 관한 문서와 일치합니다.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)); }
API의
fulfill
메서드를 실행하고AppActionsFulfillmentResult
객체를 가져옵니다.
어설션 실행
앱 작업 테스트 라이브러리를 어설션하는 데 권장하는 방법은 다음과 같습니다.
AppActionsFulfillmentResult
의 처리 유형을 어설션합니다. 예상치 못한 BII 요청이 있는 경우 앱이 어떻게 동작하는지 테스트하려면 처리 유형이FulfillmentType.INTENT
또는FulfillmentType.UNFULFILLED
여야 합니다.- 처리의 두 가지 유형은
INTENT
처리와DEEPLINK
처리입니다.- 일반적으로 개발자는 라이브러리를 트리거하여 처리 중인
shortcuts.xml
의 인텐트 태그를 보고INTENT
처리와DEEPLINK
처리를 구분할 수 있습니다. - 인텐트 태그 아래에 URL 템플릿 태그가 있으면 이는
DEEPLINK
가 이 인텐트를 처리함을 나타냅니다. - 결과 인텐트의
getData()
메서드가 null이 아닌 객체를 반환하는 경우도DEEPLINK
처리를 나타냅니다. 반대로getData
가null
을 반환하면 이는INTENT
처리입니다.
- 일반적으로 개발자는 라이브러리를 트리거하여 처리 중인
INTENT
처리의 경우AppActionsFulfillmentResult
를AppActionsIntentFulfillmentResult
로 형변환하려면getIntent
메서드를 호출하여 Android 인텐트를 가져오고 다음 중 하나를 실행합니다.- Android 인텐트의 개별 필드를 어설션합니다.
- intent.getData.getHost 메서드를 통해 액세스하는 인텐트의 URI를 어설션합니다.
DEEPLINK
처리의 경우AppActionsFulfillmentResult
를AppActionsIntentFulfillmentResult
로 형변환하려면(위의INTENT
시나리오와 동일)getIntent
메서드를 호출하여 Android 인텐트를 가져오고 딥 링크 URL을 어설션합니다(intent.getData.getHost
를 통해 액세스).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) 이상의 버전과만 호환됩니다.