How to get Spoon to take screenshots for Espresso

2019-02-06 10:11发布

4条回答
祖国的老花朵
2楼-- · 2019-02-06 10:51

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

查看更多
冷血范
3楼-- · 2019-02-06 10:54

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.

查看更多
闹够了就滚
4楼-- · 2019-02-06 10:56

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!

查看更多
叛逆
5楼-- · 2019-02-06 10:59

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.

查看更多
登录 后发表回答