ํƒœ๊ทธ ๋ณด๊ด€๋ฌผ: android-espresso

android-espresso

Espresso๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ RecyclerView ํ•ญ๋ชฉ ๋‚ด๋ถ€๋ณด๊ธฐ ํด๋ฆญ ํ•ญ๋ชฉ ์ž์ฒด๊ฐ€ ์•„๋‹Œ ํ•ด๋‹น ํ•ญ๋ชฉ ๋‚ด๋ถ€์˜ ํŠน์ •๋ณด๊ธฐ๋ฅผ

Espresso๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ RecyclerView ํ•ญ๋ชฉ ๋‚ด์˜ ํŠน์ •๋ณด๊ธฐ๋ฅผ ํด๋ฆญํ•˜๋ ค๋ฉด ์–ด๋–ป๊ฒŒ ํ•ด์•ผํ•ฉ๋‹ˆ๊นŒ? ๋‹ค์Œ์„ ์‚ฌ์šฉํ•˜์—ฌ ์œ„์น˜ 0์—์„œ ํ•ญ๋ชฉ์„ ํด๋ฆญ ํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์„ ์•Œ๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

onView(withId(R.id.recyclerView))
.perform(RecyclerViewActions.actionOnItemAtPosition(0, click()));

ํ•˜์ง€๋งŒ ํ•ญ๋ชฉ ์ž์ฒด๊ฐ€ ์•„๋‹Œ ํ•ด๋‹น ํ•ญ๋ชฉ ๋‚ด๋ถ€์˜ ํŠน์ •๋ณด๊ธฐ๋ฅผ ํด๋ฆญํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค.

๋ฏธ๋ฆฌ ๊ฐ์‚ฌ๋“œ๋ฆฝ๋‹ˆ๋‹ค.

โ€” ํŽธ์ง‘ํ•˜๋‹ค โ€”

๋” ์ •ํ™•ํ•˜๊ฒŒ ๋งํ•˜์ž๋ฉด, CardView ( ) ํ•ญ๋ชฉ ์ธ RecyclerView ( R.id.recycler_view)๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค . ๊ฐ CardView ์•ˆ์—๋Š” 4 ๊ฐœ์˜ ๋ฒ„ํŠผ (๋‹ค๋ฅธ ๊ฒƒ๋“ค ์ค‘์—์„œ)์ด ์žˆ๊ณ  ํŠน์ • ๋ฒ„ํŠผ ( ) ์„ ํด๋ฆญํ•˜๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค .R.id.card_viewR.id.bt_deliver

Espresso 2.0์˜ ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜๊ณ  ์‹ถ์ง€๋งŒ ์ด๊ฒƒ์ด ๊ฐ€๋Šฅํ•œ์ง€ ์ž˜ ๋ชจ๋ฅด๊ฒ ์Šต๋‹ˆ๋‹ค.

๊ฐ€๋Šฅํ•˜์ง€ ์•Š๋‹ค๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์‚ฌ์šฉํ•˜๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค (Thomas Keller ์ฝ”๋“œ ์‚ฌ์šฉ).

onRecyclerItemView(R.id.card_view, ???, withId(R.id.bt_deliver)).perform(click());

ํ•˜์ง€๋งŒ ๋‚˜๋Š” ๋ฌผ์Œํ‘œ์— ๋ฌด์—‡์„ ์จ์•ผํ• ์ง€ ๋ชจ๋ฅด๊ฒ ์Šต๋‹ˆ๋‹ค.



๋‹ต๋ณ€

๋ณด๊ธฐ ์ž‘์—…์„ ์‚ฌ์šฉ์ž ์ง€์ •ํ•˜์—ฌ ์ˆ˜ํ–‰ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

public class MyViewAction {

    public static ViewAction clickChildViewWithId(final int id) {
        return new ViewAction() {
            @Override
            public Matcher<View> getConstraints() {
                return null;
            }

            @Override
            public String getDescription() {
                return "Click on a child view with specified id.";
            }

            @Override
            public void perform(UiController uiController, View view) {
                View v = view.findViewById(id);
                v.performClick();
            }
        };
    }

}

๊ทธ๋Ÿฐ ๋‹ค์Œ ํด๋ฆญ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

onView(withId(R.id.rv_conference_list)).perform(
            RecyclerViewActions.actionOnItemAtPosition(0, MyViewAction.clickChildViewWithId(R.id. bt_deliver)));

๋‹ต๋ณ€

์ด์ œ android.support.test.espresso.contrib๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋” ์‰ฌ์›Œ์กŒ์Šต๋‹ˆ๋‹ค.

1) ํ…Œ์ŠคํŠธ ์ข…์†์„ฑ ์ถ”๊ฐ€

androidTestCompile('com.android.support.test.espresso:espresso-contrib:2.0') {
    exclude group: 'com.android.support', module: 'appcompat'
    exclude group: 'com.android.support', module: 'support-v4'
    exclude module: 'recyclerview-v7'
}

* ์ด๋ฏธ ๊ฐ€์ง€๊ณ ์žˆ์„ ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์œผ๋ฏ€๋กœ 3 ๊ฐœ ๋ชจ๋“ˆ ์ œ์™ธ

2) ๊ทธ๋Ÿฐ ๋‹ค์Œ ๋‹ค์Œ๊ณผ ๊ฐ™์ดํ•˜์‹ญ์‹œ์˜ค.

onView(withId(R.id.recycler_grid))
            .perform(RecyclerViewActions.actionOnItemAtPosition(0, click()));

๋˜๋Š”

onView(withId(R.id.recyclerView))
  .perform(RecyclerViewActions.actionOnItem(
            hasDescendant(withText("whatever")), click()));

๋˜๋Š”

onView(withId(R.id.recycler_linear))
            .check(matches(hasDescendant(withText("whatever"))));

๋‹ต๋ณ€

๋‹ค์Œ์€ kotlin์—์„œ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐ ํ•œ ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค.

fun clickOnViewChild(viewId: Int) = object : ViewAction {
    override fun getConstraints() = null

    override fun getDescription() = "Click on a child view with specified id."

    override fun perform(uiController: UiController, view: View) = click().perform(uiController, view.findViewById<View>(viewId))
}

๊ทธ๋ฆฌ๊ณ 

onView(withId(R.id.recyclerView)).perform(RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(position, clickOnViewChild(R.id.viewToClickInTheRow)))

๋‹ต๋ณ€

๋‹ค์Œ ์ ‘๊ทผ ๋ฐฉ์‹์„ ์‹œ๋„ํ•˜์‹ญ์‹œ์˜ค.

    onView(withRecyclerView(R.id.recyclerView)
                    .atPositionOnView(position, R.id.bt_deliver))
                    .perform(click());

    public static RecyclerViewMatcher withRecyclerView(final int recyclerViewId) {
            return new RecyclerViewMatcher(recyclerViewId);
    }

public class RecyclerViewMatcher {
    final int mRecyclerViewId;

    public RecyclerViewMatcher(int recyclerViewId) {
        this.mRecyclerViewId = recyclerViewId;
    }

    public Matcher<View> atPosition(final int position) {
        return atPositionOnView(position, -1);
    }

    public Matcher<View> atPositionOnView(final int position, final int targetViewId) {

        return new TypeSafeMatcher<View>() {
            Resources resources = null;
            View childView;

            public void describeTo(Description description) {
                int id = targetViewId == -1 ? mRecyclerViewId : targetViewId;
                String idDescription = Integer.toString(id);
                if (this.resources != null) {
                    try {
                        idDescription = this.resources.getResourceName(id);
                    } catch (Resources.NotFoundException var4) {
                        idDescription = String.format("%s (resource name not found)", id);
                    }
                }

                description.appendText("with id: " + idDescription);
            }

            public boolean matchesSafely(View view) {

                this.resources = view.getResources();

                if (childView == null) {
                    RecyclerView recyclerView =
                            (RecyclerView) view.getRootView().findViewById(mRecyclerViewId);
                    if (recyclerView != null) {

                        childView = recyclerView.findViewHolderForAdapterPosition(position).itemView;
                    }
                    else {
                        return false;
                    }
                }

                if (targetViewId == -1) {
                    return view == childView;
                } else {
                    View targetView = childView.findViewById(targetViewId);
                    return view == targetView;
                }

            }
        };
    }
}

๋‹ต๋ณ€

๋‹น์‹ ์€ ํ•  ์ˆ˜ ํด๋ฆญ ์— 3 ํ•ญ๋ชฉ ์˜ recyclerView์ฒ˜๋Ÿผ์ด :

onView(withId(R.id.recyclerView)).perform(
                RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(2,click()))

์ถ”๋ก ์ด ์‹คํŒจํ•˜์ง€ ์•Š๋„๋ก ์œ ํ˜• ์„ ์ œ๊ณตํ•˜๋Š” ๊ฒƒ์„ ์žŠ์ง€ ๋งˆ์‹ญ์‹œ์˜ค ViewHolder.


๋‹ต๋ณ€

์œ„์˜ ๋ชจ๋“  ๋‹ต๋ณ€์ด ๋‚˜๋ฅผ ์œ„ํ•ด ์ž‘๋™ํ•˜์ง€ ์•Š์•˜์œผ๋ฏ€๋กœ ์š”์ฒญ ๋œ ID๋กœ ๋ทฐ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ธฐ ์œ„ํ•ด ์…€ ๋‚ด๋ถ€์˜ ๋ชจ๋“  ๋ทฐ๋ฅผ ๊ฒ€์ƒ‰ํ•˜๋Š” ์ƒˆ๋กœ์šด ๋ฐฉ๋ฒ•์„ ๊ตฌ์ถ•ํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‘ ๊ฐ€์ง€ ๋ฐฉ๋ฒ•์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค (ํ•˜๋‚˜๋กœ ๊ฒฐํ•ฉ ๋  ์ˆ˜ ์žˆ์Œ).

fun performClickOnViewInCell(viewID: Int) = object : ViewAction {
    override fun getConstraints(): org.hamcrest.Matcher<View> = click().constraints
    override fun getDescription() = "Click on a child view with specified id."
    override fun perform(uiController: UiController, view: View) {
        val allChildViews = getAllChildrenBFS(view)
        for (child in allChildViews) {
            if (child.id == viewID) {
                child.callOnClick()
            }
        }
    }
}


private fun  getAllChildrenBFS(v: View): List<View> {
    val visited = ArrayList<View>();
    val unvisited = ArrayList<View>();
    unvisited.add(v);

    while (!unvisited.isEmpty()) {
        val child = unvisited.removeAt(0);
        visited.add(child);
        if (!(child is ViewGroup)) continue;
        val group = child
        val childCount = group.getChildCount();
        for (i in 0 until childCount) { unvisited.add(group.getChildAt(i)) }
    }

    return visited;
}

๋งˆ์ง€๋ง‰์œผ๋กœ ๋‹ค์Œ์„ ์ˆ˜ํ–‰ํ•˜์—ฌ Recycler View์—์„œ์ด๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

onView(withId(R.id.recyclerView)).perform(actionOnItemAtPosition<RecyclerView.ViewHolder>(0, getViewFromCell(R.id.cellInnerView) {
            val requestedView = it
}))

๋‹ค๋ฅธ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜๋ ค๋Š” ๊ฒฝ์šฐ ์ฝœ๋ฐฑ์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ทฐ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ฑฐ๋‚˜ ๋‹ค๋ฅธ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜๊ธฐ ์œ„ํ•ด 3-4 ๊ฐœ์˜ ๋‹ค๋ฅธ ๋ฒ„์ „์„ ๋นŒ๋“œ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


๋‹ต๋ณ€

๋‚˜๋Š” @blade์˜ ๋Œ€๋‹ต์ด ๋‚˜๋ฅผ ์œ„ํ•ด ์ž‘๋™ํ•˜์ง€ ์•Š๋Š” ์ด์œ ๋ฅผ ์ฐพ๊ธฐ ์œ„ํ•ด ๋‹ค์–‘ํ•œ ๋ฐฉ๋ฒ•์„ ๊ณ„์† ์‹œ๋„ํ–ˆ๊ณ , ๋‚ด๊ฐ€ ๊ฐ€์ง€๊ณ  ์žˆ์Œ์„ ๊นจ๋‹ซ๊ธฐ ์œ„ํ•ด OnTouchListener()๊ทธ์— ๋”ฐ๋ผ ViewAction์„ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

fun clickTopicToWeb(id: Int): ViewAction {

        return object : ViewAction {
            override fun getDescription(): String {...}

            override fun getConstraints(): Matcher<View> {...}

            override fun perform(uiController: UiController?, view: View?) {

                view?.findViewById<View>(id)?.apply {

                    //Generalized for OnClickListeners as well
                    if(isEnabled && isClickable && !performClick()) {
                        //Define click event
                        val event: MotionEvent = MotionEvent.obtain(
                          SystemClock.uptimeMillis(),
                          SystemClock.uptimeMillis(),
                          MotionEvent.ACTION_UP,
                          view.x,
                          view.y,
                          0)

                        if(!dispatchTouchEvent(event))
                            throw Exception("Not clicking!")
                    }
                }
            }
        }
    }