Compose의 UI 테스트에서 시맨틱을 사용해 Modifier 속성값 가져와보기

JhoonP·2023년 8월 8일
0
post-thumbnail

Jetpack Compose와 Flutter는 같은 선언형 프로그래밍 방식으로 UI 코드를 작성하기에, UI 테스트 코드 작성 또한 유사한 방식으로 작동할 줄 알았으나 생각보다 어려운 부분이 많았다.

Flutter에서의 UI 테스트 코드 작성

먼저, Flutter의 위젯 단위는 Widget 이라는 class이다. 따라서 type을 가지기 때문에 UI 테스트 코드에서 특정 타입의 위젯만 골라내는 것이 가능하다.

이하부터 UI Component 단위를 위젯 이라고 부르도록 하겠다.

//	basic_page.dart
class BasicPage extends StatelessWidget {
  final String? title;
  // ...
  
  
  Widget build(BuildContext context) {
  	// ...
  }
}

//	basic_page_test.dart
void main() {
	group('BasicPage', () {
      testWidgets('Check title', (tester) async {
        await tester.pumpWidget(BasicPage(child: Container(),));
        await tester.pump(const Duration(milliseconds: 100));

        final basicPage = tester.widget<BasicPage>(find.byType(BasicPage));
        print("BasicPage title: ${basicPage.title}");
      });
    }
}

위와 같이 원하는 타입의 위젯을 골라 찾아낼 수 있고, 위젯이 가진 프로퍼티까지 편하게 접근할 수 있다.

Compose에서의 UI 테스트 코드 작성

하지만 Compose에서 원하는 위젯을 찾아내고 위젯을 구성하는 값을 가져오는 것은 Flutter와는 다른 과정이 필요하다.

Compose의 위젯은 @Composable 함수로 구성한다. 함수이기 때문에 타입을 가지지 않고 프로퍼티 또한 존재하지 않는다.

이러한 Composable 위젯을 테스트 코드에서 접근하는 방법은 Semantics을 활용한다.
Semantics는 Composition에서 접근성(Accessibility)과 테스팅을 구현하기 위해 별도로 구성되는 트리이다.

기본 Compose 위젯(Text, Image 등)이나 Material library의 위젯들은 몇 가지 기본적인 Semantics를 가지고 있다. 예를 들어 Texttext를, ImagecontentDescription를 가진다.

이러한 Semantics를 개발자가 직접 주입해 테스팅에 사용할 수도 있다.

//	CustomUi.kt
@Composable
fun CustomUi(modifier = Modifier) {
	val borderWidth = if(isFocused || hasError) 1.dp else 0.dp
	val borderColor = if(hasError) warningBorder else neutralBorderTertiary
 	Row(
    	modifier = modifier
          .border(borderWidth, borderColor,  RoundedCornerShape(8.dp))
          .padding(containerPadding)
          .semantics {
              testTag = "ContainerModifier"
          },
      verticalAlignment = Alignment.CenterVertically
      ) {
  		// ...
    }
}

위 코드를 보면, CustomUiRowModifiersemantics 함수를 사용해 testTagContainerModifier로 설정했다.

//	CustomUiTest.kt
class CustomUiTest {
	@get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun test() {
        with(composeTestRule) {
            setContent {
                CustomUi()
            }

            run  {
                val rowSemantics = onNodeWithTag("ContainerModifier")
                rowSemantics.assertExists()
            }
        }
    }
}

테스트 코드에서는 onNodeWithTag 함수로 앞에서 설정한 testTag를 가진 시멘틱 노드를 찾을 수 있고, 이를 사용해 위젯 존재 여부(assertExists) 등을 검사할 수 있다.

이렇게 직접 값을 주입하고 위젯을 찾는 방법도 있지만, 특정 텍스트를 포함한 위젯, 스크롤 가능한 위젯 쉽게 찾을 수 있는 여러 방법들도 제공한다.

하지만 Flutter와 같이 type-safe한 방식으로 위젯 혹은 위젯의 속성에 접근하는 방법은 없는듯하여, 위젯의 Modifier에 접근해 border width, padding 값을 가져올 수 없다.

그렇다고 아얘 테스트 코드에서 위젯 속성을 가져올 수 없는건 아니고, 시맨틱 프로퍼티(Semantic Properties)를 활용하면 가능하다.

val borderWidth = if(isFocused || hasError) 1.dp else 0.dp
val borderColor = if(hasError) warningBorder else 
Row(
    modifier = modifier
        .border(borderWidth, borderColor,  RoundedCornerShape(8.dp))
            shape = RoundedCornerShape(8.dp))
        .padding(containerPadding)
        .semantics {
            testTag = "ContainerModifier"
            this[SemanticsPropertyKey(name = "borderWidthKey")] = borderWidth
            this[SemanticsPropertyKey(name = "borderColorKey")] = borderColor
        },
    verticalAlignment = Alignment.CenterVertically
)

semantics은 키, 값 쌍을 이루는 시맨틱 프로퍼티를 저장할 수 있는 set 함수를 제공하고 [ ] 문법으로 접근할 수 있다. SemanticsPropertyKey를 사용해 키를 생성해 값을 할당할 수 있다.
위와 같이 borderWidthKey 키에 Modifier에 설정한 borderWidth 값을 할당하면,

@Test
fun test() {
    with(composeTestRule) {
        setContent {
            CustomUi()
        }

        run  {
			val borderWidth = onNodeWithTag("ContainerModifier")
				.fetchSemanticsNode()
                .config.findLast { it.key.name == "borderWidthKey" }?.value as Dp
			assert(borderWidth == 0.dp)
        }
	}
}

테스트 코드를 보면, 시맨틱 노드에서 fetchSemanticsNode 함수로 SemanticConfiguration에 접근할 수 있다. 여기서 원하는 시맨틱 프로퍼티를 키 값으로 가져올 수 있다.
borderWidthKey 키로 가져온 borderWidthDp로 형변환 해주면 테스트 코드에 사용할 수 있다.

이렇게 Compose에서는 시맨틱을 사용해서 다양한 테스트 코드를 작성할 수 있다. 비록 Flutter 보다는 불편한 부분이 있는건지, 내가 더 효율적인 방법을 찾지 못한 것인지는 모르겠지만 Compose에서도 여러가지 UI 테스트를 수행할 수 있었다.


참고

Semantics in Compose: https://developer.android.com/jetpack/compose/semantics
Testing your Compose layout: https://developer.android.com/jetpack/compose/testing
Testing cheatsheet: https://developer.android.com/jetpack/compose/testing-cheatsheet

profile
배울게 끝이 없네 끝이 없어

0개의 댓글