Tài liệu này mô tả cách thiết lập nhiều quy trình kiểm thử Espresso phổ biến.
Khớp một chế độ xem với một chế độ xem khác
Một bố cục có thể chứa một số khung hiển thị nhất định không phải là duy nhất. Ví dụ: nút gọi lặp lại trong một bảng danh bạ có thể có cùng R.id
, chứa cùng văn bản và có cùng thuộc tính như các nút gọi khác trong hệ phân cấp khung hiển thị.
Ví dụ: trong hoạt động này, thành phần hiển thị có văn bản "7"
lặp lại trên nhiều hàng:
Thông thường, thành phần hiển thị không phải duy nhất sẽ được ghép nối với một số nhãn duy nhất ở bên cạnh, chẳng hạn như tên của người liên hệ bên cạnh nút gọi. Trong trường hợp này, bạn có thể sử dụng trình so khớp hasSibling()
để thu hẹp lựa chọn:
Kotlin
onView(allOf(withText("7"), hasSibling(withText("item: 0")))) .perform(click())
Java
onView(allOf(withText("7"), hasSibling(withText("item: 0")))) .perform(click());
Khớp với khung hiển thị bên trong thanh thao tác
ActionBarTestActivity
có hai thanh thao tác khác nhau: thanh thao tác thông thường và thanh thao tác theo ngữ cảnh được tạo từ trình đơn tuỳ chọn. Cả hai thanh thao tác đều có một mục luôn hiển thị và hai mục chỉ hiển thị trong trình đơn mục bổ sung. Khi được nhấp vào một mục, mục đó sẽ thay đổi TextView thành nội dung của mục đã nhấp.
Việc so khớp các biểu tượng hiển thị trên cả hai thanh thao tác rất đơn giản, như minh hoạ trong đoạn mã sau đây:
Kotlin
fun testClickActionBarItem() { // We make sure the contextual action bar is hidden. onView(withId(R.id.hide_contextual_action_bar)) .perform(click()) // Click on the icon - we can find it by the r.Id. onView(withId(R.id.action_save)) .perform(click()) // Verify that we have really clicked on the icon // by checking the TextView content. onView(withId(R.id.text_action_bar_result)) .check(matches(withText("Save"))) }
Java
public void testClickActionBarItem() { // We make sure the contextual action bar is hidden. onView(withId(R.id.hide_contextual_action_bar)) .perform(click()); // Click on the icon - we can find it by the r.Id. onView(withId(R.id.action_save)) .perform(click()); // Verify that we have really clicked on the icon // by checking the TextView content. onView(withId(R.id.text_action_bar_result)) .check(matches(withText("Save"))); }
Mã này trông giống hệt với thanh thao tác theo ngữ cảnh:
Kotlin
fun testClickActionModeItem() { // Make sure we show the contextual action bar. onView(withId(R.id.show_contextual_action_bar)) .perform(click()) // Click on the icon. onView((withId(R.id.action_lock))) .perform(click()) // Verify that we have really clicked on the icon // by checking the TextView content. onView(withId(R.id.text_action_bar_result)) .check(matches(withText("Lock"))) }
Java
public void testClickActionModeItem() { // Make sure we show the contextual action bar. onView(withId(R.id.show_contextual_action_bar)) .perform(click()); // Click on the icon. onView((withId(R.id.action_lock))) .perform(click()); // Verify that we have really clicked on the icon // by checking the TextView content. onView(withId(R.id.text_action_bar_result)) .check(matches(withText("Lock"))); }
Thao tác nhấp vào các mục trong trình đơn mục bổ sung sẽ phức tạp hơn một chút đối với thanh thao tác thông thường vì một số thiết bị có nút trình đơn mục bổ sung phần cứng giúp mở các mục mục bổ sung trong trình đơn tuỳ chọn, và một số thiết bị có nút trình đơn mục bổ sung phần mềm để mở trình đơn mục bổ sung thông thường. Thật may là Espresso xử lý việc đó cho chúng tôi.
Đối với thanh thao tác thông thường:
Kotlin
fun testActionBarOverflow() { // Make sure we hide the contextual action bar. onView(withId(R.id.hide_contextual_action_bar)) .perform(click()) // Open the options menu OR open the overflow menu, depending on whether // the device has a hardware or software overflow menu button. openActionBarOverflowOrOptionsMenu( ApplicationProvider.getApplicationContext<Context>()) // Click the item. onView(withText("World")) .perform(click()) // Verify that we have really clicked on the icon by checking // the TextView content. onView(withId(R.id.text_action_bar_result)) .check(matches(withText("World"))) }
Java
public void testActionBarOverflow() { // Make sure we hide the contextual action bar. onView(withId(R.id.hide_contextual_action_bar)) .perform(click()); // Open the options menu OR open the overflow menu, depending on whether // the device has a hardware or software overflow menu button. openActionBarOverflowOrOptionsMenu( ApplicationProvider.getApplicationContext()); // Click the item. onView(withText("World")) .perform(click()); // Verify that we have really clicked on the icon by checking // the TextView content. onView(withId(R.id.text_action_bar_result)) .check(matches(withText("World"))); }
Đây là giao diện của giao diện trên các thiết bị có nút trình đơn mục bổ sung phần cứng:
Đối với thanh thao tác theo ngữ cảnh, việc này thực sự dễ dàng:
Kotlin
fun testActionModeOverflow() { // Show the contextual action bar. onView(withId(R.id.show_contextual_action_bar)) .perform(click()) // Open the overflow menu from contextual action mode. openContextualActionModeOverflowMenu() // Click on the item. onView(withText("Key")) .perform(click()) // Verify that we have really clicked on the icon by // checking the TextView content. onView(withId(R.id.text_action_bar_result)) .check(matches(withText("Key"))) } }
Java
public void testActionModeOverflow() { // Show the contextual action bar. onView(withId(R.id.show_contextual_action_bar)) .perform(click()); // Open the overflow menu from contextual action mode. openContextualActionModeOverflowMenu(); // Click on the item. onView(withText("Key")) .perform(click()); // Verify that we have really clicked on the icon by // checking the TextView content. onView(withId(R.id.text_action_bar_result)) .check(matches(withText("Key"))); } }
Để xem mã đầy đủ cho các mẫu này, hãy xem mẫu ActionBarTest.java
trên GitHub.
Xác nhận rằng một thành phần hiển thị không hiển thị
Sau khi thực hiện một loạt hành động, chắc chắn bạn sẽ muốn xác nhận trạng thái của giao diện người dùng đang được kiểm thử. Đôi khi, đây có thể là trường hợp tiêu cực, chẳng hạn như khi điều gì đó không xảy ra. Xin lưu ý rằng bạn có thể chuyển mọi trình so khớp khung hiển thị hamcrest thành ViewAssertion
bằng cách sử dụng ViewAssertions.matches()
.
Trong ví dụ bên dưới, chúng tôi sẽ lấy trình so khớp isDisplayed()
và đảo ngược trình so khớp này bằng trình so khớp not()
tiêu chuẩn:
Kotlin
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withId import org.hamcrest.Matchers.not onView(withId(R.id.bottom_left)) .check(matches(not(isDisplayed())))
Java
import static androidx.test.espresso.Espresso.onView; import static androidx.test.espresso.assertion.ViewAssertions.matches; import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; import static androidx.test.espresso.matcher.ViewMatchers.withId; import static org.hamcrest.Matchers.not; onView(withId(R.id.bottom_left)) .check(matches(not(isDisplayed())));
Phương pháp trên sẽ có hiệu quả nếu khung hiển thị vẫn là một phần của hệ phân cấp. Nếu không, bạn sẽ nhận được NoMatchingViewException
và cần sử dụng ViewAssertions.doesNotExist()
.
Xác nhận rằng không có thành phần hiển thị
Nếu khung hiển thị đã biến mất khỏi hệ phân cấp khung hiển thị (có thể xảy ra khi một hành động dẫn đến việc chuyển đổi sang một hoạt động khác), bạn nên sử dụng ViewAssertions.doesNotExist()
:
Kotlin
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.assertion.ViewAssertions.doesNotExist import androidx.test.espresso.matcher.ViewMatchers.withId onView(withId(R.id.bottom_left)) .check(doesNotExist())
Java
import static androidx.test.espresso.Espresso.onView; import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist; import static androidx.test.espresso.matcher.ViewMatchers.withId; onView(withId(R.id.bottom_left)) .check(doesNotExist());
Xác nhận rằng một mục dữ liệu không nằm trong bộ chuyển đổi
Để chứng minh một mục dữ liệu cụ thể không nằm trong AdapterView
, bạn phải thực hiện các bước khác một chút. Chúng tôi phải tìm AdapterView
mà chúng tôi quan tâm
và truy vấn dữ liệu mà công cụ đó nắm giữ. Chúng ta không cần phải sử dụng onData()
.
Thay vào đó, chúng ta sử dụng onView()
để tìm AdapterView
rồi sử dụng một trình so khớp khác để xử lý dữ liệu bên trong khung hiển thị này.
Đầu tiên là trình so khớp:
Kotlin
private fun withAdaptedData(dataMatcher: Matcher<Any>): Matcher<View> { return object : TypeSafeMatcher<View>() { override fun describeTo(description: Description) { description.appendText("with class name: ") dataMatcher.describeTo(description) } public override fun matchesSafely(view: View) : Boolean { if (view !is AdapterView<*>) { return false } val adapter = view.adapter for (i in 0 until adapter.count) { if (dataMatcher.matches(adapter.getItem(i))) { return true } } return false } } }
Java
private static Matcher<View> withAdaptedData(final Matcher<Object> dataMatcher) { return new TypeSafeMatcher<View>() { @Override public void describeTo(Description description) { description.appendText("with class name: "); dataMatcher.describeTo(description); } @Override public boolean matchesSafely(View view) { if (!(view instanceof AdapterView)) { return false; } @SuppressWarnings("rawtypes") Adapter adapter = ((AdapterView) view).getAdapter(); for (int i = 0; i < adapter.getCount(); i++) { if (dataMatcher.matches(adapter.getItem(i))) { return true; } } return false; } }; }
Sau đó, chúng ta chỉ cần onView()
để tìm AdapterView
:
Kotlin
fun testDataItemNotInAdapter() { onView(withId(R.id.list)) .check(matches(not(withAdaptedData(withItemContent("item: 168"))))) } }
Java
@SuppressWarnings("unchecked") public void testDataItemNotInAdapter() { onView(withId(R.id.list)) .check(matches(not(withAdaptedData(withItemContent("item: 168"))))); } }
Ngoài ra, chúng tôi có một khẳng định sẽ không thành công nếu một mục bằng "item: 168" tồn tại trong khung hiển thị bộ chuyển đổi với danh sách mã nhận dạng.
Để xem mẫu đầy đủ, hãy xem phương thức testDataItemNotInAdapter()
trong lớp AdapterViewTest.java
trên GitHub.
Sử dụng trình xử lý lỗi tuỳ chỉnh
Việc thay thế FailureHandler
mặc định trong Espresso bằng một thành phần tuỳ chỉnh cho phép xử lý thêm hoặc xử lý lỗi khác, chẳng hạn như chụp ảnh màn hình hoặc truyền thêm thông tin gỡ lỗi.
Ví dụ CustomFailureHandlerTest
minh hoạ cách triển khai một trình xử lý lỗi tuỳ chỉnh:
Kotlin
private class CustomFailureHandler(targetContext: Context) : FailureHandler { private val delegate: FailureHandler init { delegate = DefaultFailureHandler(targetContext) } override fun handle(error: Throwable, viewMatcher: Matcher<View>) { try { delegate.handle(error, viewMatcher) } catch (e: NoMatchingViewException) { throw MySpecialException(e) } } }
Java
private static class CustomFailureHandler implements FailureHandler { private final FailureHandler delegate; public CustomFailureHandler(Context targetContext) { delegate = new DefaultFailureHandler(targetContext); } @Override public void handle(Throwable error, Matcher<View> viewMatcher) { try { delegate.handle(error, viewMatcher); } catch (NoMatchingViewException e) { throw new MySpecialException(e); } } }
Trình xử lý lỗi này sẽ gửi một MySpecialException
thay vì NoMatchingViewException
và uỷ quyền tất cả các lỗi khác cho DefaultFailureHandler
. Bạn có thể đăng ký CustomFailureHandler
với Espresso trong phương thức setUp()
của quy trình kiểm thử:
Kotlin
@Throws(Exception::class) override fun setUp() { super.setUp() getActivity() setFailureHandler(CustomFailureHandler( ApplicationProvider.getApplicationContext<Context>())) }
Java
@Override public void setUp() throws Exception { super.setUp(); getActivity(); setFailureHandler(new CustomFailureHandler( ApplicationProvider.getApplicationContext())); }
Để biết thêm thông tin, hãy xem giao diện FailureHandler
và
Espresso.setFailureHandler()
.
Nhắm mục tiêu cửa sổ không mặc định
Android hỗ trợ nhiều cửa sổ. Thông thường, giá trị này minh bạch đối với người dùng và nhà phát triển ứng dụng. Tuy nhiên, trong một số trường hợp, nhiều cửa sổ sẽ hiển thị, chẳng hạn như khi cửa sổ tự động hoàn thành được vẽ trên cửa sổ chính của ứng dụng trong tiện ích tìm kiếm. Để đơn giản hoá mọi thứ, theo mặc định, Espresso sử dụng phương pháp phỏng đoán để đoán ra Window
mà bạn định tương tác. Phương pháp phỏng đoán này hầu như luôn đủ tốt; tuy nhiên, trong một số ít trường hợp, bạn sẽ cần chỉ định cửa sổ mà tương tác nên nhắm đến. Bạn có thể thực hiện việc này bằng cách cung cấp trình so khớp cửa sổ gốc riêng hoặc trình so khớp Root
:
Kotlin
onView(withText("South China Sea")) .inRoot(withDecorView(not(`is`(getActivity().getWindow().getDecorView())))) .perform(click())
Java
onView(withText("South China Sea")) .inRoot(withDecorView(not(is(getActivity().getWindow().getDecorView())))) .perform(click());
Tương tự như trường hợp với ViewMatchers
, chúng tôi cung cấp một tập hợp RootMatchers
được cung cấp trước.
Tất nhiên, bạn luôn có thể triển khai đối tượng Matcher
của riêng mình.
Hãy xem mẫu MultiplexWindowTest trên GitHub.
Khớp với đầu trang hoặc chân trang trong chế độ xem danh sách
Đầu trang và chân trang được thêm vào ListViews
bằng phương thức addHeaderView()
và addFooterView()
. Để đảm bảo Espresso.onData()
biết cần so khớp đối tượng dữ liệu nào, hãy nhớ truyền một giá trị đối tượng dữ liệu đặt trước ở dạng tham số thứ hai đến addHeaderView()
và addFooterView()
. Ví dụ:
Kotlin
const val FOOTER = "FOOTER" ... val footerView = layoutInflater.inflate(R.layout.list_item, listView, false) footerView.findViewById<TextView>(R.id.item_content).text = "count:" footerView.findViewById<TextView>(R.id.item_size).text = data.size.toString listView.addFooterView(footerView, FOOTER, true)
Java
public static final String FOOTER = "FOOTER"; ... View footerView = layoutInflater.inflate(R.layout.list_item, listView, false); footerView.findViewById<TextView>(R.id.item_content).setText("count:"); footerView.findViewById<TextView>(R.id.item_size).setText(String.valueOf(data.size())); listView.addFooterView(footerView, FOOTER, true);
Sau đó, bạn có thể viết trình so khớp cho chân trang:
Kotlin
import org.hamcrest.Matchers.allOf import org.hamcrest.Matchers.instanceOf import org.hamcrest.Matchers.`is` fun isFooter(): Matcher<Any> { return allOf(`is`(instanceOf(String::class.java)), `is`(LongListActivity.FOOTER)) }
Java
import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; @SuppressWarnings("unchecked") public static Matcher<Object> isFooter() { return allOf(is(instanceOf(String.class)), is(LongListActivity.FOOTER)); }
Việc tải khung hiển thị trong một chương trình kiểm thử không đơn giản:
Kotlin
import androidx.test.espresso.Espresso.onData import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.sample.LongListMatchers.isFooter fun testClickFooter() { onData(isFooter()) .perform(click()) // ... }
Java
import static androidx.test.espresso.Espresso.onData; import static androidx.test.espresso.action.ViewActions.click; import static androidx.test.espresso.sample.LongListMatchers.isFooter; public void testClickFooter() { onData(isFooter()) .perform(click()); // ... }
Hãy xem mã mẫu đầy đủ trong phương thức testClickFooter()
của AdapterViewTest.java
trên GitHub.