关于 Espresso
Espresso 是一个简单好用的 Android UI 测试框架
Espresso 主要由以下三个基础部分组成:
ViewMatchers - 在当前 View 层级去匹配指定的 View .
ViewActions - 执行 Views 的某些行为, 如点击事件 .
ViewAssertions - 检查 Views 的某些状态, 如是否显示 .
Espresso 使用示例
- onView(ViewMatcher) //1. 匹配 View
- .perform(ViewAction) //2. 执行 View 行为
- .check(ViewAssertion); //3. 验证 View
复制代码
准备
第一步. build.gradle 添加如下依赖:
- androidTestImplementation 'com.android.support.test.espresso:espresso-core:latest.version'
- androidTestImplementation 'com.android.support.test:runner:latest.version'
- androidTestImplementation 'com.android.support.test:rules:latest.version'
复制代码
第二步. android.defaultConfig 添加如下配置
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
复制代码
基础用法
获得 View
withId 方式
onView(withId(R.id.my_view))
复制代码
withText 方式
onView(withText("Hello World!"))
复制代码
执行 View 行为
点击
onView(...).perform(click());
复制代码
文本内容输入
onView(...).perform(typeText("Hello"), click());
复制代码
scrollTo 滑动
onView(...).perform(scrollTo(), click());
复制代码
检验 View
检验 View 的文本内容
onView(...).check(matches(withText("Hello!")));
复制代码
检验 View 的显示状态
onView(...).check(matches(isDisplayed()));
复制代码
示例场景:
简单的登 6 场景测试
- @RunWith(AndroidJUnit4.class)
- public class LoginUITest {
- @Rule
- public ActivityTestRule<LoginActivity> rule=new ActivityTestRule<LoginActivity>(LogingActivity.class,true);
- @Test
- public void login(){
- //login
- onView(withId(R.id.userName)).perform(typeText("Jack"),closeSoftKeyboard());
- onView(withId(R.id.password)).perform(typeText("1234"),closeSoftKeyboard());
- onView(withText("登录")).perform(click());
- //verify
- onView(withId(R.id.content)).check(matches(isDisplayed()));
- }
- }
复制代码
进阶用法
1. 使用 IdlingResource
通常, 我们实际的应用当中会有很多异步任务, 例如网络请求, 图片加载等, 但是 Espresso 并不知道你的异步任务什么时候结束, 所以需要借助于 IdlingResource .
这里需要注意的是, 如果你是通过 AsyncTask 或者 AsyncTaskCompat 方式的异步任务, Espresso 已经处理好, 并不需要去额外的处理.
第一步: 添加依赖库
compile 'com.android.support.test.espresso:espresso-idling-resource:latest.version'
复制代码
第二步: 定义一个 IdlingResource 接口 .
- public interface IdlingResource {
- /**
- * 用来标识 IdlingResource 名称
- */
- public String getName();
- /**
- * 当前 IdlingResource 是否空闲 .
- */
- public boolean isIdleNow();
- /**
- 注册一个空闲状态变换的 ResourceCallback 回调
- */
- public void registerIdleTransitionCallback(ResourceCallback callback);
- /**
- * 通知 Espresso 当前 IdlingResource 状态变换为空闲的回调接口
- */
- public interface ResourceCallback {
- /**
- * 当前状态转变为空闲时, 调用该方法告诉 Espresso
- */
- public void onTransitionToIdle();
- }
- }
复制代码
下面我们以一个示例来说明: 场景: 假设当前我们需要测试用户的头像的是否正常显示.
Activity 代码
- public class AvatarActivity extends AppCompatActivity{
- private ImageView mAvatar;
- public static SimpleIdlingResource sIdlingResource=new SimpleIdlingResource();
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_idling_resource);
- mAvatar= (ImageView) findViewById(R.id.avatar);
- //App 开始进入忙碌状态 , 等待通知
- sIdlingResource.increment();
- // 开始加载头像图片
- Glide.with(this).load("https://avatars2.githubusercontent.com/u/2297803?v=3&s=460").into(new GlideDrawableImageViewTarget(mAvatar) {
- @Override
- public void onResourceReady(GlideDrawable resource,
- GlideAnimation<? super GlideDrawable> animation) {
- super.onResourceReady(resource, animation);
- // 加载完毕后, 将 App 设置成空闲状态
- sIdlingResource.decrement();
- }
- });
- }
- }
复制代码
SimpleIdlingResource 代码
- public final class SimpleIdlingResource implements IdlingResource {
- private final AtomicInteger counter = new AtomicInteger(0);
- private volatile ResourceCallback resourceCallback;
- @Override
- public String getName() {
- return "SimpleIdlingResource";
- }
- /**
- * 如果 counter 的值等于 0, 说明当前是空闲状态
- */
- @Override
- public boolean isIdleNow() {
- return counter.get() == 0;
- }
- @Override
- public void registerIdleTransitionCallback(ResourceCallback resourceCallback) {
- this.resourceCallback = resourceCallback;
- }
- /**
- * counter 的值增长方法
- */
- public void increment() {
- counter.getAndIncrement();
- }
- /**
- *counter 的值减少方法
- */
- public void decrement() {
- int counterVal = counter.decrementAndGet();
- if (counterVal == 0) {
- // 执行 onTransitionToIdle() 方法, 告诉 Espresso, 当前是空闲状态.
- if (null != resourceCallback) {
- resourceCallback.onTransitionToIdle();
- }
- }
- if (counterVal <0) {
- throw new IllegalArgumentException("Counter has been corrupted!");
- }
- }
- }
复制代码
IdlingResourceTest 代码
- @RunWith(AndroidJUnit4.class)
- public class IdlingResourceTest {
- @Rule
- public ActivityTestRule<AvatarActivity> rule=new ActivityTestRule<>(AvatarActivity.class,true);
- @Before
- public void registerIdlingResource(){
- Espresso.registerIdlingResources(rule.getActivity().sIdlingResource);
- }
- @Test
- public void avatarDisplayed(){
- onView(withId(R.id.avatar)).check(matches(isDisplayed()));
- }
- @After
- public void unregisterIdlingResource() {
- Espresso.unregisterIdlingResources(
- rule.getActivity().sIdlingResource);
- }
- }
复制代码
另外, Espresso 提供了一个实现好的 CountingIdlingResource 类, 所以如果没有特别需求的话, 直接使用 CountingIdlingResource 即可.
2. 创建一个自定义 Espresso matcher
目前 Espresso 提供的方法基本上可以满足你的测试需求, 如下图所示:
如果你需要对自定义的 View 中某个自定义属性进行测试的话, 你可以创建一个自定义的 Matcher
- public static Matcher<View> isMoved() {
- return new TypeSafeMatcher<View>() {
- @Override
- public void describeTo(Description description) {
- description.appendText("is moved");
- }
- @Override
- public boolean matchesSafely(View view) {
- return ((CustomView)view).isMoved();
- }
- };
- }
复制代码
3. 如何处理动画
系统动画: 为了避免动画线程运行期间对 Espresso 测试产生的影响, 官方强烈建议关闭系统动画. 如图所示:
自定义动画: 对于自定义动画, 开发者可以借助以下代码去控制动画的开和关.
该方法可以监听系统动画的开关事件, 这是一个值得推荐的做法, 不仅仅是在 Espresso 测试中.
- boolean animationEnabled=true;
- if (Build.VERSION.SDK_INT>=Build.VERSION_CODES.JELLY_BEAN_MR1){
- try {
- if (Settings.Global.getFloat(getContentResolver(),Settings.Global.ANIMATOR_DURATION_SCALE)==0.0f){
- animationEnabled=false;
- }
- } catch (Settings.SettingNotFoundException e) {
- e.printStackTrace();
- }
- }
复制代码
4. 优雅的 IntentTest
普通的方式
- @Rule
- public ActivityTestRule<MainActivity> rule=new ActivityTestRule<MainActivity>(MainActivity.class){
- @Override
- protected Intent getActivityIntent() {
- Intent result=new Intent(...);
- result.putExtra(...)
- return result;
- }
- };
复制代码
如果使用普通的方式, 那么该单元测试类下的所有测试都是基于该 Intent 启动的 Activity, 这样显然不够灵活.
优雅的方式
- @Rule
- public ActivityTestRule<MainActivity> rule=new ActivityTestRule<MainActivity>(MainActivity.class,true,false);// 第三个参数为是否自动运行 Activity
- public void myTest(){
- Intent result=new Intent(...);
- result.putExtra(...);
- rule.launchActivity(result);
- }
复制代码
5. 保证测试的独立性
通常使用 Espresso 进行 UI 测试的时候, 你并不期望去测试网络或者远程的服务相关的东西, 所以, 你可以借助于 Espresso Intent,Mockito for mocking, 以及依赖注射, Dagger2. 总之, 尽量去分离那些不属于 UI 层面的内容.
例如: 你需要某个按钮的点击事件进行测试, 但是该按钮的点击后, 会跳转到别的 Activity, 可是你不希望去测试别的 Activity, 那么这里就可以通过拦截 Intent 来解决.
首先你需要添加 IntentTest 依赖库:
androidTestCompile 'com.android.support.test.espresso:espresso-intents:2.2.2'
复制代码
InterceptIntentTest 示例代码
- @RunWith(AndroidJUnit4.class)
- public class InterceptIntentTest {
- @Rule
- public IntentsTestRule<MainActivity> rule=new IntentsTestRule<>(MainActivity.class);
- @Before
- public void stubCameraIntent(){
- // 模拟一个 ActivityResult
- Instrumentation.ActivityResult result = createImageCaptureActivityResultStub();
- // 拦截 MediaStore.ACTION_IMAGE_CAPTURE, 并返回模拟后的 result
- intending(hasAction(MediaStore.ACTION_IMAGE_CAPTURE)).respondWith(result);
- }
- @Test
- public void takePhoto_cameraIsLaunched(){
- onView(withId(R.id.button_take_photo)).perform(click());
- intended(hasAction(MediaStore.ACTION_IMAGE_CAPTURE));
- ...
- }
- private Instrumentation.ActivityResult createImageCaptureActivityResultStub() {
- // Create the ActivityResult, with a null Intent since we do not want to return any data
- // back to the Activity.
- return new Instrumentation.ActivityResult(Activity.RESULT_OK, null);
- }
- }
复制代码
6. 避免直接复制粘贴 test 代码
举个简单的例子.
onData(allOf(is(instanceOf(Map.class)),hasEntry(equalTo("STR"),is("item:50")))).perform(click());
复制代码
以上这段代码是匹配列表中符合条件的 item, 并执行执行点击事件, 测试也正常, 同样这段代码也被复制到了其他的测试方法中使用, 这时, 设想一下, 如果你的 adapter 中的数据源改成了 cursor 或者其他, 于是悲催了, 你需要修改很多地方, 显然, 这不是一个合格的 CV 战士.
所以, 我们需要对之前那行代码进行改装:
- @Test
- public void myTest(){
- onData(withItemContent("item:50")).perform(click());
- }
- public static Matcher<? extends Object> withItemContent(String expectedText) {
- ....
- }
复制代码
很简单, 只需将可能变化的部分抽出来即可.
7. 如何测试 View 的位置
如图所示:
8. 自定义错误日志
默认的错误日志打印信息比较多, 如图:
如果你只想显示你关心的日志信息, 你可以自定义 FailureHandler:
- 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);
- }
- }
- }
复制代码
- @Override
- public void setUp() throws Exception {
- super.setUp();
- getActivity();
- setFailureHandler(new CustomFailureHandler(getInstrumentation()
- .getTargetContext()));
- }
复制代码
关于我
来源: https://juejin.im/post/5b9d100a5188255c9446257f