androidx.a2ui.compose:compose-ui-testing 테스트 라이브러리는 탐색의 TestNavHostController과 같이 Jetpack 테스트 라이브러리에 적합한 컨트롤러 패턴을 사용하는 테스트 API를 제공합니다.
정적 매개변수를 사용하고 UI를 내보내는 표준 Jetpack Compose 구성요소와 달리 A2UI 구성요소는 컨텍스트를 기반으로 합니다. A2uiComponentScope를 사용하여 동적 데이터 바인딩을 평가하고, 아웃바운드 작업을 에이전트에 디스패치하고, 양방향 데이터 바인딩에 다시 쓰고, 동적 하위 템플릿을 확장합니다.
테스트 API는 실제 A2uiMessageProcessor 인스턴스를 프로비저닝하고 Compose 테스트 환경에 바인드된 코루틴을 실행하면서 테스트 설정을 간소화합니다.
격리된 구성요소
개별 구성요소가 디자인 시스템 테마 내에서 데이터를 확인하고, 작업을 디스패치하고, 올바르게 렌더링하는지 확인할 수 있습니다.
@Test
fun button_resolvesStubChildAndDispatchesAction() = runComposeUiTest {
// 1. Create the test controller
val controller = A2uiTestController(
// Provide a catalog containing the component under test
catalog = CustomComponentCatalog,
// Configure the component under test with concrete properties
initialComponents = listOf(
A2uiComponentPayload(
id = "root",
type = "Button",
properties = mapOf(
"child" to "btn_text",
"variant" to "primary",
"action" to mapOf(
"event" to mapOf(
"name" to "submit_form",
"context" to mapOf("username" to mapOf("path" to "/user/name")),
),
),
),
),
A2uiComponentPayload("btn_text"),
),
// Stub the required child component
componentStubs = listOf(
A2uiComponentStub.withId("btn_text") { _, modifier ->
Text("Submit", modifier = modifier)
},
),
// Provide initial dynamic data
initialData = mapOf("user" to mapOf("name" to "Test User")),
)
// 2. Start background processing and initialize the surface
val surface = controller.start()
// 3. Mount the UI
setContent {
A2uiTestSurface(surface)
}
// 4. Interact using standard Compose UI semantics
onNodeWithText("Submit").performClick()
// 5. Wait for Compose and A2UI background processes to settle
waitForIdle()
controller.waitForIdle()
// 6. Assert outbound actions were correctly evaluated and intercepted
val action = controller.dispatchedActions.single() as A2uiEventAction
assertEquals("submit_form", action.eventName)
assertEquals("Test User", action.context["username"])
}
표면 상태
상태와 전환을 포함하여 A2uiSurface와 같은 서피스 호스트를 테스트할 수 있습니다.
@Test
fun surface_displaysLoading_thenTransitionsToContent() = runComposeUiTest {
// 1. Create an empty controller to simulate a pending network request
val controller = A2uiTestController(
catalog = CustomComponentCatalog,
// Pre-register a stub for the expected root component type
componentStubs = listOf(
A2uiComponentStub.withType("RootLayout") { _, modifier ->
Text("Content Ready", modifier = modifier)
},
),
)
val surface = controller.start()
// 2. Mount the surface UI
setContent {
A2uiSurface(surfaceModel = surface)
}
// 3. Assert the loading placeholder is active
onNode(hasProgressBarRangeInfo(ProgressBarRangeInfo.Indeterminate)).assertExists()
// 4. Simulate the agent pushing the layout payload over the network
controller.updateComponent(
id = "root",
type = "RootLayout",
properties = emptyMap(),
)
// 5. Wait for the data layer and animation to settle
controller.waitForIdle()
waitForIdle()
// 6. Assert the loading state is gone and content is visible
onNode(hasProgressBarRangeInfo(ProgressBarRangeInfo.Indeterminate)).assertDoesNotExist()
onNodeWithText("Content Ready").assertIsDisplayed()
}
양방향 바인딩
사용자 입력 중에 데이터 모델에 다시 쓰는 텍스트 필드와 같은 구성요소를 테스트하고 에이전트가 데이터 모델을 변경할 때 반응형 업데이트를 확인할 수 있습니다.
@Test
fun textField_writesToDataModelAndReactsToAgent() = runComposeUiTest {
val controller = A2uiTestController(
catalog = CustomComponentCatalog,
initialComponents = listOf(
A2uiComponentPayload(
id = "root",
type = "TextField",
properties = mapOf(
"label" to "Username",
"value" to mapOf("path" to "/form/username"),
),
),
),
initialData = mapOf("form" to mapOf("username" to "Initial")),
)
val surface = controller.start()
setContent {
A2uiTestSurface(surface)
}
// 1. User interaction updates the global DataModel locally
onNodeWithText("Initial").performTextReplacement("LocallyTyped")
waitForIdle()
// 2. Assert the component wrote back to the DataModel
assertEquals("LocallyTyped", controller.getData<String>("/form/username"))
// 3. Simulate the agent pushing a data update for the same path
controller.updateData("/form/username", "ServerOverridden")
controller.waitForIdle()
// 4. Assert the component reactively updated the UI
onNodeWithText("ServerOverridden").assertIsDisplayed()
}
템플릿이 적용된 하위 요소가 있는 구성요소
A2UI ChildList 템플릿을 사용하여 정의된 하위 요소 컬렉션을 표시하도록 설계된 구성요소를 테스트할 수 있습니다.
@Test
fun column_rendersDynamicChildTemplates() = runComposeUiTest {
val controller = A2uiTestController(
catalog = CustomComponentCatalog,
initialData = mapOf(
"catalog" to mapOf(
"products" to listOf(
mapOf("title" to "Camera"),
mapOf("title" to "Laptop"),
),
),
),
initialComponents = listOf(
A2uiComponentPayload(
id = "root",
type = "Column",
properties = mapOf(
"children" to mapOf(
"path" to "/catalog/products",
"componentId" to "product_template",
),
),
),
// Bind the initial properties for the dynamically instantiated
// template stub.
A2uiComponentPayload(
id = "product_template",
properties = mapOf("title" to mapOf("path" to "title")),
),
),
componentStubs = listOf(
A2uiComponentStub.withId(id = "product_template") { props, modifier ->
val titleProp = remember { A2uiProperty.dynamicString("title") }
val title = props.bind(titleProp) ?: "Unknown"
Text(text = "Stubbed: $title", modifier = modifier)
},
),
)
val surface = controller.start()
setContent { A2uiTestSurface(surface) }
// Verify the template was instantiated twice with relative data
onNodeWithText("Stubbed: Camera").assertExists()
onNodeWithText("Stubbed: Laptop").assertExists()
// Simulate appending a new item to the data model array
controller.updateData("/catalog/products/-", mapOf("title" to "Tablet"))
controller.waitForIdle()
// Verify the Column dynamically instantiated a new child stub
onNodeWithText("Stubbed: Tablet").assertExists()
}
상담사 오류의 오류 대체
다음과 같이 서피스와 구성요소가 환각과 같은 에이전트 오류를 적절하게 처리하는지 확인할 수 있습니다.
@Test
fun surface_displaysErrorFallback_onAgentHallucination() = runComposeUiTest {
val controller = A2uiTestController(catalog = CustomComponentCatalog)
val surface = controller.start()
// 1. Mount the surface orchestrator with error boundaries
setContent { A2uiSurface(surfaceModel = surface) }
// 2. Simulate an agent hallucinating a broken component layout
controller.failComponent(
id = "root",
exception = A2uiException.A2uiValidationException(
message = "HallucinatedType",
path = "/components/root"
),
)
controller.waitForIdle()
// 3. Assert the surface displayed the fallback error state
onNodeWithText("Failed to load: HallucinatedType").assertIsDisplayed()
// 4. Assert the core layer dispatched an error to the server
val errorMsg = controller.outboundErrors.single()
assertEquals("VALIDATION_FAILED", errorMsg.code)
}
점진적 렌더링
상위 구성요소는 로드되었지만 하위 구성요소는 아직 대기 중인 중간 상태를 테스트할 수 있습니다.
@Test
fun progressiveRendering_parentRendersWhileChildIsPending() = runComposeUiTest {
// 1. Mount the parent, omitting the child instance
val controller = A2uiTestController(
catalog = CustomComponentCatalog,
initialComponents = listOf(
A2uiComponentPayload(
id = "root",
type = "Button",
properties = mapOf(
"child" to "delayed_text_id",
"action" to mapOf("event" to mapOf("name" to "click")),
),
),
),
)
val surface = controller.start()
setContent {
A2uiTestSurface(surface)
}
// 2. Initial state: parent is rendered, child displays loading state
onNodeWithText("Submit").assertDoesNotExist()
onNode(hasProgressBarRangeInfo(ProgressBarRangeInfo.Indeterminate)).assertExists()
// 3. Simulate arrival of the child component
controller.updateComponent(
id = "delayed_text_id",
type = "Text",
properties = mapOf("text" to "Submit"),
)
controller.waitForIdle()
// 4. Assert that progressive rendering completed
onNode(hasProgressBarRangeInfo(ProgressBarRangeInfo.Indeterminate)).assertDoesNotExist()
onNodeWithText("Submit").assertIsDisplayed()
}
구현 세부정보
다음 섹션에서는 테스트 프레임워크의 구성요소 재정의, 스키마 검증, 코루틴 동기화를 설명합니다.
테스트 라이브러리에는 다음과 같은 기본 API가 도입되었습니다.
A2uiTestController: 확장 프로그램 생성자 함수 및 기본 테스트 컨트롤러 인터페이스A2uiComponentStub: 하위 및 카탈로그 구성요소의 스텁 및 재정의A2uiTestSurface: 테스트 화면을 마운트하는 경량 컴포저블 유틸리티입니다.
구성요소 재정의와 표준 모의 비교
무거운 서드 파티 모의 프레임워크를 없애기 위해 UI 스텁 (A2uiComponentStub)을 사용하여 하위 구성요소와 외부 종속 항목을 우회합니다. A2uiComponentStub.withId는 ID로 특정 구성요소 인스턴스를 가로채고 A2uiComponentStub.withType는 전체 카탈로그 유형의 렌더링을 재정의합니다.
빠른 실패 스키마 검사
테스트 프레임워크는 A2UI 프로토콜 계약을 동기식으로 적용합니다. 컨트롤러가 구성요소를 초기화하거나 업데이트하면 제공된 페이로드에 대해 A2uiCoreSchemaValidator를 실행합니다. 필수 필드가 누락되거나 유형이 일치하지 않는 등 잘못된 속성이 설정되면 테스트가 A2uiValidationException와 함께 즉시 비정상 종료됩니다.
코루틴 동기화
A2uiTestController.start은 runComposeUiTest()에서 제공하는 테스트 코루틴 컨텍스트에 연결됩니다. currentCoroutineContext()를 추출하고, 백그라운드 루프를 분리된 Job에 매핑하고, 테스트 블록이 완료되면 자동으로 취소되어 매달린 테스트 실행을 방지합니다. waitForIdle()는 대기 중인 모든 백그라운드 코루틴이 완료되기를 기다립니다.