问题
I've been trying to follow the instructions on getting Spoon 1.1.14 to take screenshots for failing Espresso tests.
What's the best way to configure this with a custom Espresso FailureHandler?
回答1:
Here's how I am doing this at the moment:
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()));
}
}
My base Espresso test class sets up the custom FailureHandler (I like using a base class to hold lots of other common code):
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;
}
}
}
}
...and here is the slightly modified screenshot code from the Gist posted by 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));
}
}
I'd love to find a way to avoid calling setUpFailureHandler()
in every test - please let me know if you have a good idea on how to avoid this!
回答2:
Based on @Eric's approach above, and with ActivityTestRule we can obtain the current test method name and test class name from description
object when apply()
function is called. By overriding the apply function like this
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 ... */
}
I was able to take screenshot with correct test method and test class so it can be correctly integrated into final Spoon test report. And remember to use the JUnit4 runner by adding
@RunWith(AndroidJUnit4.class)
to your test class.
回答3:
You could try setting this up in your subclass of ActivityRule
. Something like
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();
}
}
I'm not sure that testClassName
and testMethodName
will always be correct. The way I'm fetching those seems super-fragile, but I couldn't figure out a better way.
回答4:
Replacing the default FailureHandler of Espresso with a custom one allows for additional error handling e.g. taking a screenshot:
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);
}
}
Also, you'll need to throw the custom exception in the test setup and teardown:
@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()));
}
You can use this in your Espresso test like:
public void testWithCustomFailureHandler() {
try {
onView(withText("does not exist")).perform(click());
} catch (MySpecialException expected) {
Log.e(TAG, "Special exception is special and expected: ", expected);
}
}
Please look at the Android official CustomFailure example:
Click here for the official example
Click here for another example
来源:https://stackoverflow.com/questions/29929082/how-to-get-spoon-to-take-screenshots-for-espresso-tests