问题 如何让Spoon为Espresso测试拍摄截图?


我一直在努力追随 获取Spoon 1.1.14获取失败的Espresso测试截图的说明

用a配置它的最佳方法是什么? 自定义Espresso FailureHandler


6134
2018-04-28 20:16


起源



答案:


这就是我现在这样做的方式:

public class MainScreenTest extends BaseStatelessBlackBoxEspressoTest<LaunchActivity> {

    public MainScreenTest() {
        super(LaunchActivity.class);
    }

    public void testMainScreen() {
        // Unfortunately this must be explicitly called in each test :-(
        setUpFailureHandler();

        onView(withId(R.id.main_circle)).
                check(matches(isDisplayed()));
    }
}

我的基础Espresso测试类设置自定义FailureHandler(我喜欢使用基类来保存许多其他常用代码):

public abstract class BaseStatelessBlackBoxEspressoTest<T extends Activity> extends BaseBlackBoxTest<T> {

    public BaseStatelessBlackBoxEspressoTest(Class clazz) {
        super(clazz);
    }

    @Before
    public void setUp() throws Exception {
        super.setUp();
        getActivity();
    }

    public void setUpFailureHandler() {
        // Get the test class and method.  These have to match those of the test
        // being run, otherwise the screenshot will not be displayed in the Spoon 
        // HTML output.  We cannot call this code directly in setUp, because at 
        // that point the current test method is not yet in the stack.
        StackTraceElement[] trace = Thread.currentThread().getStackTrace();
        String testClass = trace[3].getClassName();
        String testMethod = trace[3].getMethodName();

        Espresso.setFailureHandler(new CustomFailureHandler(
                getInstrumentation().getTargetContext(),
                testClass,
                testMethod));
    }

    private static class CustomFailureHandler implements FailureHandler {
        private final FailureHandler mDelegate;
        private String mClassName;
        private String mMethodName;

        public CustomFailureHandler(Context targetContext, String className, String methodName) {
            mDelegate = new DefaultFailureHandler(targetContext);
            mClassName = className;
            mMethodName = methodName;
        }

        @Override
        public void handle(Throwable error, Matcher<View> viewMatcher) {
            try {
                mDelegate.handle(error, viewMatcher);
            } catch (Exception e) {
                SpoonScreenshotAction.perform("espresso_assertion_failed", mClassName, mMethodName);
                throw e;
            }
        }
    }
}

...这里是稍微修改过的截图代码 Square发布的要点

/**
 * Source: https://github.com/square/spoon/issues/214#issuecomment-81979248
 */
public final class SpoonScreenshotAction implements ViewAction {
    private final String tag;
    private final String testClass;
    private final String testMethod;

    public SpoonScreenshotAction(String tag, String testClass, String testMethod) {
        this.tag = tag;
        this.testClass = testClass;
        this.testMethod = testMethod;
    }

    @Override
    public Matcher<View> getConstraints() {
        return Matchers.anything();
    }

    @Override
    public String getDescription() {
        return "Taking a screenshot using spoon.";
    }

    @Override
    public void perform(UiController uiController, View view) {
        Spoon.screenshot(getActivity(view), tag, testClass, testMethod);
    }

    private static Activity getActivity(View view) {
        Context context = view.getContext();
        while (!(context instanceof Activity)) {
            if (context instanceof ContextWrapper) {
                context = ((ContextWrapper) context).getBaseContext();
            } else {
                throw new IllegalStateException("Got a context of class "
                        + context.getClass()
                        + " and I don't know how to get the Activity from it");
            }
        }
        return (Activity) context;
    }    

    public static void perform(String tag, String className, String methodName) {
        onView(isRoot()).perform(new SpoonScreenshotAction(tag, className, methodName));
    }
}

我很想找到一种避免打电话的方法 setUpFailureHandler() 在每次测试中 - 如果您对如何避免这种情况有一个好主意,请告诉我!


7
2018-04-28 20:26





基于@ Eric的上述方法,以及 ActivityTestRule 我们可以从中获取当前的测试方法名称和测试类名称 description 对象何时 apply() 函数被调用。通过覆盖这样的apply函数

public class MyActivityTestRule<T extends Activity> extends ActivityTestRule<T> {

  @Override
  public Statement apply(Statement base, Description description) {
    String testClassName = description.getClassName();
    String testMethodName = description.getMethodName();
    Context context =  InstrumentationRegistry.getTargetContext();
    Espresso.setFailureHandler(new FailureHandler() {
      @Override public void handle(Throwable throwable, Matcher<View> matcher) {
        SpoonScreenshotAction.perform("failure", testClassName, testMethodName);
        new DefaultFailureHandler(context).handle(throwable, matcher);
        }
    });
    return super.apply(base, description);
  }

  /* ... other useful things ... */
}

我能够使用正确的测试方法和测试类获取屏幕截图,以便可以将其正确地集成到最终的Spoon测试报告中。并记得通过添加使用JUnit4 runner

@RunWith(AndroidJUnit4.class)

到你的考试班。


7
2018-06-14 23:43



使用SpoonScreenshotAction本身会触发Espresso FailureHandler,例如没有找到活动。这很糟糕,因为这会导致无限循环,从而导致OutOfMemory错误。最好直接使用Spoon.screenshot(): Spoon.screenshot(getActivity(), tag, testClassName, testMethodName); 而不是SpoonScreenshotAction.perform(); - sebokopter


答案:


这就是我现在这样做的方式:

public class MainScreenTest extends BaseStatelessBlackBoxEspressoTest<LaunchActivity> {

    public MainScreenTest() {
        super(LaunchActivity.class);
    }

    public void testMainScreen() {
        // Unfortunately this must be explicitly called in each test :-(
        setUpFailureHandler();

        onView(withId(R.id.main_circle)).
                check(matches(isDisplayed()));
    }
}

我的基础Espresso测试类设置自定义FailureHandler(我喜欢使用基类来保存许多其他常用代码):

public abstract class BaseStatelessBlackBoxEspressoTest<T extends Activity> extends BaseBlackBoxTest<T> {

    public BaseStatelessBlackBoxEspressoTest(Class clazz) {
        super(clazz);
    }

    @Before
    public void setUp() throws Exception {
        super.setUp();
        getActivity();
    }

    public void setUpFailureHandler() {
        // Get the test class and method.  These have to match those of the test
        // being run, otherwise the screenshot will not be displayed in the Spoon 
        // HTML output.  We cannot call this code directly in setUp, because at 
        // that point the current test method is not yet in the stack.
        StackTraceElement[] trace = Thread.currentThread().getStackTrace();
        String testClass = trace[3].getClassName();
        String testMethod = trace[3].getMethodName();

        Espresso.setFailureHandler(new CustomFailureHandler(
                getInstrumentation().getTargetContext(),
                testClass,
                testMethod));
    }

    private static class CustomFailureHandler implements FailureHandler {
        private final FailureHandler mDelegate;
        private String mClassName;
        private String mMethodName;

        public CustomFailureHandler(Context targetContext, String className, String methodName) {
            mDelegate = new DefaultFailureHandler(targetContext);
            mClassName = className;
            mMethodName = methodName;
        }

        @Override
        public void handle(Throwable error, Matcher<View> viewMatcher) {
            try {
                mDelegate.handle(error, viewMatcher);
            } catch (Exception e) {
                SpoonScreenshotAction.perform("espresso_assertion_failed", mClassName, mMethodName);
                throw e;
            }
        }
    }
}

...这里是稍微修改过的截图代码 Square发布的要点

/**
 * Source: https://github.com/square/spoon/issues/214#issuecomment-81979248
 */
public final class SpoonScreenshotAction implements ViewAction {
    private final String tag;
    private final String testClass;
    private final String testMethod;

    public SpoonScreenshotAction(String tag, String testClass, String testMethod) {
        this.tag = tag;
        this.testClass = testClass;
        this.testMethod = testMethod;
    }

    @Override
    public Matcher<View> getConstraints() {
        return Matchers.anything();
    }

    @Override
    public String getDescription() {
        return "Taking a screenshot using spoon.";
    }

    @Override
    public void perform(UiController uiController, View view) {
        Spoon.screenshot(getActivity(view), tag, testClass, testMethod);
    }

    private static Activity getActivity(View view) {
        Context context = view.getContext();
        while (!(context instanceof Activity)) {
            if (context instanceof ContextWrapper) {
                context = ((ContextWrapper) context).getBaseContext();
            } else {
                throw new IllegalStateException("Got a context of class "
                        + context.getClass()
                        + " and I don't know how to get the Activity from it");
            }
        }
        return (Activity) context;
    }    

    public static void perform(String tag, String className, String methodName) {
        onView(isRoot()).perform(new SpoonScreenshotAction(tag, className, methodName));
    }
}

我很想找到一种避免打电话的方法 setUpFailureHandler() 在每次测试中 - 如果您对如何避免这种情况有一个好主意,请告诉我!


7
2018-04-28 20:26





基于@ Eric的上述方法,以及 ActivityTestRule 我们可以从中获取当前的测试方法名称和测试类名称 description 对象何时 apply() 函数被调用。通过覆盖这样的apply函数

public class MyActivityTestRule<T extends Activity> extends ActivityTestRule<T> {

  @Override
  public Statement apply(Statement base, Description description) {
    String testClassName = description.getClassName();
    String testMethodName = description.getMethodName();
    Context context =  InstrumentationRegistry.getTargetContext();
    Espresso.setFailureHandler(new FailureHandler() {
      @Override public void handle(Throwable throwable, Matcher<View> matcher) {
        SpoonScreenshotAction.perform("failure", testClassName, testMethodName);
        new DefaultFailureHandler(context).handle(throwable, matcher);
        }
    });
    return super.apply(base, description);
  }

  /* ... other useful things ... */
}

我能够使用正确的测试方法和测试类获取屏幕截图,以便可以将其正确地集成到最终的Spoon测试报告中。并记得通过添加使用JUnit4 runner

@RunWith(AndroidJUnit4.class)

到你的考试班。


7
2018-06-14 23:43



使用SpoonScreenshotAction本身会触发Espresso FailureHandler,例如没有找到活动。这很糟糕,因为这会导致无限循环,从而导致OutOfMemory错误。最好直接使用Spoon.screenshot(): Spoon.screenshot(getActivity(), tag, testClassName, testMethodName); 而不是SpoonScreenshotAction.perform(); - sebokopter


您可以尝试在您的子类中进行设置 ActivityRule。就像是

return new Statement() {
  @Override public void evaluate() throws Throwable {
    final String testClassName = description.getTestClass().getSimpleName();
    final String testMethodName = description.getMethodName();
    Instrumentation instrumentation = fetchInstrumentation();
    Context context = instrumentation.getTargetContext();
    Espresso.setFailureHandler(new FailureHandler() {
      @Override public void handle(Throwable throwable, Matcher<View> matcher) {
        SpoonScreenshotAction.perform("failure", testClassName, testMethodName);
        new DefaultFailureHandler(context).handle(throwable, matcher);
      }
    });
    base.evaluate();
  }
} 

我不确定 testClassName 和 testMethodName 将 总是 是对的。我拿这些的方式似乎超级脆弱,但我无法找到更好的方法。


2
2018-04-28 21:55





使用自定义的Espresso替换Espresso的默认FailureHandler允许进行其他错误处理,例如截图:

private static class CustomFailureHandler implements FailureHandler {
@Override
  public void handle(Throwable error, Matcher<View> viewMatcher) {
    throw new MySpecialException(error);
}
  }
  private static class MySpecialException extends RuntimeException {
  MySpecialException(Throwable cause) {
     super(cause);
    }
}

此外,您需要在测试设置和拆卸中抛出自定义异常:

 @Override
   public void setUp() throws Exception {
   super.setUp();
   getActivity();
   setFailureHandler(new CustomFailureHandler());
  }

@Override
  public void tearDown() throws Exception {
  super.tearDown();
  Espresso.setFailureHandler(new DefaultFailureHandler(getTargetContext()));
  }

你可以在Espresso测试中使用它,如:

public void testWithCustomFailureHandler() {
  try {
  onView(withText("does not exist")).perform(click());
} catch (MySpecialException expected) {
  Log.e(TAG, "Special exception is special and expected: ", expected);
  }
}

请查看Android官方CustomFailure示例:
点击此处查看官方示例

点击这里再看一个例子


0
2018-04-29 15:18