Ответ 1

Вот как я это делаю в данный момент:

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;
            }
        }
    }
}

... и вот немного измененный код скриншота из Gist, отправленный 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() в каждом тесте - сообщите мне, если у вас есть хорошая идея о том, как этого избежать!

Ответ 2

В соответствии с приведенным выше подходом @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, добавив

@RunWith(AndroidJUnit4.class)

для вашего тестового класса.

Ответ 3

Вы можете попробовать установить это в своем подклассе 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 всегда будут правильными. То, как я получаю их, кажется супер-хрупким, но я не мог найти лучшего способа.

Ответ 4

Замена стандартного 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()));
  }

Вы можете использовать это в своем тесте эспрессо, например:

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

Пожалуйста, ознакомьтесь с официальным примером CustomFailure для Android:
Нажмите здесь для официального примера

Нажмите здесь для другого примера