Android Espresso의 Matcher의 역할 및 동작 원리 분석

Matchers

Matcher는 에스프레소에서 UI의 요소가 의도된 대로 출력되었는지 확인하는데 사용되는 객체입니다. 일반적으로 TextView의 text가 "No country string"와 동일한지 체크하는 코틀린 코드는 다음과 같이 작성할 수 있습니다.

onView(withId(R.id.tvLocale))
        .check(ViewAssertions.matches(ViewMatchers.withText("No country string")))

ViewAssertions.matches(...)는 인자로 주어진 Matcher에 정의된 기준으로 View의 요소가 의도된대로 설정되었는지 판단할 수 있습니다. 만약 기준에 부합하지 못한다면 Assertion을 발생시킵니다.

간단히 위의 코드가 어떻게 동작하는지 Espresso 코드를 분석해보겠습니다.

ViewAssertions 분석

ViewAssertion.matches(..)는 MatchesViewAssertion 객체를 리턴합니다.

public static ViewAssertion matches(final Matcher<? super View> viewMatcher) {
  return new MatchesViewAssertion(checkNotNull(viewMatcher));
}

onView(...).check()는 내부적으로 MatchesViewAssertion.check()를 호출합니다. 이 메소드는 먼저 일치하지 않을 때 보여줄 description을 미리 생성하고 assertThat()을 호출합니다.

실제로 assertThat() 내부에서 View의 text와 인자로 넘어온 text를 비교하여 assert를 발생시킬지 말지 결정합니다. assert가 발생한다면 미리 만들어둔 description을 이용하여 에러 로그를 출력합니다.

static class MatchesViewAssertion implements ViewAssertion {

  private MatchesViewAssertion(final Matcher<? super View> viewMatcher) {
    this.viewMatcher = viewMatcher;
  }

  @Override
  public void check(View view, NoMatchingViewException noViewException) {
      ....
      description.appendText("' doesn't match the selected view.");
      assertThat(description.toString(), view, viewMatcher);
    }
  }
  ...
}

ViewMatchers 분석

위에서 assertThat()은 static 메소드인 ViewMatchers.assertThat()입니다. 코드를 보면 matcher.matches(actual)가 false이면 assertion이 발생합니다. 이제 Matcher.matches()의 코드만 확인하면 됩니다. Matcher 내부에 text를 비교하는 구현부가 존재할 것으로 예상이 되네요.

public static <T> void assertThat(String message, T actual, Matcher<T> matcher) {
  if (!matcher.matches(actual)) {
    Description description = new StringDescription();
    description
        .appendText(message)
        .appendText("\nExpected: ")
        .appendDescriptionOf(matcher)
        .appendText("\n     Got: ");
    if (actual instanceof View) {
      description.appendValue(HumanReadables.describe((View) actual));
    } else {
      description.appendValue(actual);
    }
    description.appendText("\n");
    throw new AssertionFailedError(description.toString());
  }
}

Matcher 분석

앞에서 인자로 ViewMatchers.withText("No country string")를 전달하였는데요. ViewMatchers.withText()는 WithTextMatcher 객체를 생성하여 리턴합니다.

public static Matcher<View> withText(final Matcher<String> stringMatcher) {
  return new WithTextMatcher(checkNotNull(stringMatcher));
}

WithTextMatcher 객체는 BoundedMatcher를 상속하는 객체인데요. Text를 비교하는 내용은 matchesSafely()에 구현되어 있고, Matcher.matches() 내부에서 matchesSafely()를 호출하는 구조입니다.

static final class WithTextMatcher extends BoundedMatcher<View, TextView> {
  .....
  @Override
  public void describeTo(Description description) {
    description.appendText("with text: ");
    stringMatcher.describeTo(description);
  }

  @Override
  protected boolean matchesSafely(TextView textView) {
    String text = textView.getText().toString();
    if (stringMatcher.matches(text)) {
      return true;
    } else if (textView.getTransformationMethod() != null) {
      CharSequence transformedText =
          textView.getTransformationMethod().getTransformation(text, textView);
      if (transformedText != null) {
        return stringMatcher.matches(transformedText.toString());
      }
    }
    return false;
  }

  @Override
  public final boolean matches(Object item) {
      ......
      return matchesSafely((S) item);
    }
    return false;
  }
}

정리

ViewAssertion.check()Matcher.check()의 true/false 결과에 따라 assertion을 발생할지 결정합니다. View의 어떤 요소를 어떻게 비교할지는 Matcher.check()의 구현부에 달려있으며, Custom Matcher를 만들어 사용할 수도 있습니다.

참고

Custom Matcher를 직접 구현하는 방법은 Android Espresso, Custom Matcher 구현 방법을 참고해주세요.

Espresso로 테스트 코드를 작성하는 기본적인 방법은 다음 글들을 참고해주세요.

Loading script...

Related Posts

codechachaCopyright ©2019 codechacha