EspressoはThread.sleep();
は必要ないと主張していますが、それを含めない限り私のコードは機能しません。 IPに接続しています。接続中に、進行状況ダイアログが表示されます。ダイアログが閉じるのを待つためにsleep
が必要です。これは、私が使用するテストスニペットです。
IP.enterIP(); // fills out an IP dialog (this is done with espresso)
//progress dialog is now shown
Thread.sleep(1500);
onView(withId(R.id.button).perform(click());
私はこのコードを試してみましたwithおよびwithoutThread.sleep();
ですが、R.id.Button
は存在しません。私がそれを機能させることができる唯一の方法は、睡眠をとることです。
また、Thread.sleep();
をgetInstrumentation().waitForIdleSync();
のようなものに置き換えてみましたが、まだ運がありません。
これがこれを行う唯一の方法ですか?それとも何か不足していますか?
前もって感謝します。
私の考えでは、正しいアプローチは次のようになります。
/** Perform action of waiting for a specific view id. */
public static ViewAction waitId(final int viewId, final long millis) {
return new ViewAction() {
@Override
public Matcher<View> getConstraints() {
return isRoot();
}
@Override
public String getDescription() {
return "wait for a specific view with id <" + viewId + "> during " + millis + " millis.";
}
@Override
public void perform(final UiController uiController, final View view) {
uiController.loopMainThreadUntilIdle();
final long startTime = System.currentTimeMillis();
final long endTime = startTime + millis;
final Matcher<View> viewMatcher = withId(viewId);
do {
for (View child : TreeIterables.breadthFirstViewTraversal(view)) {
// found view with required ID
if (viewMatcher.matches(child)) {
return;
}
}
uiController.loopMainThreadForAtLeast(50);
}
while (System.currentTimeMillis() < endTime);
// timeout happens
throw new PerformException.Builder()
.withActionDescription(this.getDescription())
.withViewDescription(HumanReadables.describe(view))
.withCause(new TimeoutException())
.build();
}
};
}
そして、使用パターンは次のようになります。
// wait during 15 seconds for a view
onView(isRoot()).perform(waitId(R.id.dialogEditor, TimeUnit.SECONDS.toMillis(15)));
AlexKのすばらしい回答に感謝します。コードを多少遅延させる必要がある場合があります。サーバーの応答を必ずしも待っているわけではありませんが、アニメーションが完了するのを待っている可能性があります。私は個人的にEspresso idolingResourcesに問題があります(単純なことのために多くのコード行を書いていると思います)ので、AlexKのやり方を次のコードに変更しました。
/**
* Perform action of waiting for a specific time.
*/
public static ViewAction waitFor(final long millis) {
return new ViewAction() {
@Override
public Matcher<View> getConstraints() {
return isRoot();
}
@Override
public String getDescription() {
return "Wait for " + millis + " milliseconds.";
}
@Override
public void perform(UiController uiController, final View view) {
uiController.loopMainThreadForAtLeast(millis);
}
};
}
したがって、Delay
クラスを作成し、このメソッドをその中に配置して、簡単にアクセスできるようにすることができます。 Testクラスでも同じように使用できます:onView(isRoot()).perform(waitFor(5000));
サーバーの応答を待っていて、応答に基づいて要素の可視性を変更する同様の問題への答えを探しているときに、このスレッドに出くわしました。
上記の解決策は間違いなく役に立ちましたが、最終的に chiukiのこの優れた例 を見つけ、アプリのアイドル期間中にアクションが発生するのを待つたびに、そのアプローチを頼りにしています。
ElapsedTimeIdlingResource() を自分のユーティリティクラスに追加しました。これをEspresso固有の代替として効果的に使用できるようになりました。
// Make sure Espresso does not time out
IdlingPolicies.setMasterPolicyTimeout(waitingTime * 2, TimeUnit.MILLISECONDS);
IdlingPolicies.setIdlingResourceTimeout(waitingTime * 2, TimeUnit.MILLISECONDS);
// Now we wait
IdlingResource idlingResource = new ElapsedTimeIdlingResource(waitingTime);
Espresso.registerIdlingResources(idlingResource);
// Stop and verify
onView(withId(R.id.toggle_button))
.check(matches(withText(R.string.stop)))
.perform(click());
onView(withId(R.id.result))
.check(matches(withText(success ? R.string.success: R.string.failure)));
// Clean up
Espresso.unregisterIdlingResources(idlingResource);
この行を追加する方が簡単だと思います:
SystemClock.sleep(1500);
戻る前に、指定されたミリ秒数(uptimeMillis)待機します。 sleep(long)に似ていますが、InterruptedExceptionをスローしません。 interrupt()イベントは、次の割り込み可能な操作まで延期されます。少なくとも指定されたミリ秒数が経過するまで戻りません。
バリスタメソッドを使用できます。
BaristaSleepActions.sleep(2000);
BaristaSleepActions.sleep(2, SECONDS);
バリスタは、Espressoをラップして、受け入れられた回答に必要なすべてのコードを追加しないライブラリです。そして、ここにリンクがあります! https://github.com/SchibstedSpain/Barista
私はコーディングとEspressoに慣れていないので、アイドリングを使用することが適切で合理的な解決策であることは知っていますが、それを行うにはまだ十分な知性がありません。
もっと知識が得られるまで、テストを何とか実行する必要があります。そのため、今のところ、要素を見つけるために何度も試行し、見つかった場合は停止し、見つからない場合は短時間スリープして開始するこの汚いソリューションを使用しています再び最大試行回数に達するまで(これまでの最大試行回数は約150回でした)。
private static boolean waitForElementUntilDisplayed(ViewInteraction element) {
int i = 0;
while (i++ < ATTEMPTS) {
try {
element.check(matches(isDisplayed()));
return true;
} catch (Exception e) {
e.printStackTrace();
try {
Thread.sleep(WAITING_TIME);
} catch (Exception e1) {
e.printStackTrace();
}
}
}
return false;
}
ID、テキスト、親などで要素を検索するすべてのメソッドでこれを使用しています:
static ViewInteraction findById(int itemId) {
ViewInteraction element = onView(withId(itemId));
waitForElementUntilDisplayed(element);
return element;
}
Espressoは、テストでのsleep()呼び出しを回避するために構築されています。テストでは、IPを入力するダイアログを開いてはなりません。これは、テストされたアクティビティの責任です。
一方、UIテストでは次のことを行う必要があります。
テストは次のようになります。
// type the IP and press OK
onView (withId (R.id.dialog_ip_edit_text))
.check (matches(isDisplayed()))
.perform (typeText("IP-TO-BE-TYPED"));
onView (withText (R.string.dialog_ok_button_title))
.check (matches(isDisplayed()))
.perform (click());
// now, wait for the button and click it
onView (withId (R.id.button))
.check (matches(isDisplayed()))
.perform (click());
Espressoは、テストを実行する前に、UIスレッドとAsyncTaskプールの両方で発生しているすべてが完了するのを待ちます。
テストでは、アプリケーションの責任とは関係ないことを忘れないでください。 「十分な情報に基づいたユーザー」のように動作する必要があります。クリックして、画面に何かが表示されていることを確認しますが、実際にはコンポーネントのIDを知っているユーザー
Espresso Idling Resourceを使用する必要があります。これは CodeLab で提案されています
アイドリングリソースは、その結果がUIテストの後続の操作に影響する非同期操作を表します。アイドリングリソースをEspressoに登録することにより、アプリのテスト時にこれらの非同期操作をより確実に検証できます。
プレゼンターからの非同期呼び出しの例
@Override
public void loadNotes(boolean forceUpdate) {
mNotesView.setProgressIndicator(true);
if (forceUpdate) {
mNotesRepository.refreshData();
}
// The network request might be handled in a different thread so make sure Espresso knows
// that the app is busy until the response is handled.
EspressoIdlingResource.increment(); // App is busy until further notice
mNotesRepository.getNotes(new NotesRepository.LoadNotesCallback() {
@Override
public void onNotesLoaded(List<Note> notes) {
EspressoIdlingResource.decrement(); // Set app as idle.
mNotesView.setProgressIndicator(false);
mNotesView.showNotes(notes);
}
});
}
依存関係
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
implementation 'androidx.test.espresso:espresso-idling-resource:3.1.1'
Androidxの場合
androidTestImplementation 'com.Android.support.test.espresso:espresso-core:3.0.2'
implementation 'com.Android.support.test.espresso:espresso-idling-resource:3.0.2'
公式リポジトリ:https://github.com/googlecodelabs/Android-testing
IdlingResourceの例:https://github.com/googlesamples/Android-testing/tree/master/ui/espresso/IdlingResourceSample =
これは this answer に似ていますが、試行の代わりにタイムアウトを使用し、他のViewInteractionsとチェーンできます:
/**
* Wait for view to be visible
*/
fun ViewInteraction.waitUntilVisible(timeout: Long): ViewInteraction {
val startTime = System.currentTimeMillis()
val endTime = startTime + timeout
do {
try {
check(matches(isDisplayed()))
return this
} catch (e: NoMatchingViewException) {
Thread.sleep(50)
}
} while (System.currentTimeMillis() < endTime)
throw TimeoutException()
}
使用法:
onView(withId(R.id.whatever))
.waitUntilVisible(5000)
.perform(click())
これは、Androidテスト用にKotlinで使用しているヘルパーです。私の場合、サーバーの応答を模倣するためにlongOperationを使用していますが、目的に合わせて調整できます。
@Test
fun ensureItemDetailIsCalledForRowClicked() {
onView(withId(R.id.input_text))
.perform(ViewActions.typeText(""), ViewActions.closeSoftKeyboard())
onView(withId(R.id.search_icon)).perform(ViewActions.click())
longOperation(
longOperation = { Thread.sleep(1000) },
callback = {onView(withId(R.id.result_list)).check(isVisible())})
}
private fun longOperation(
longOperation: ()-> Unit,
callback: ()-> Unit
){
Thread{
longOperation()
callback()
}.start()
}
私のユーティリティは、エラーなしで通過するか、タイムアウト後にスロー可能オブジェクトをスローするまで、実行可能または呼び出し可能実行を繰り返します。エスプレッソテストに最適です!
最後のビューインタラクション(ボタンクリック)がいくつかのバックグラウンドスレッド(ネットワーク、データベースなど)をアクティブにするとします。その結果、新しい画面が表示され、次のステップで確認する必要がありますが、新しい画面をいつテストできるようになるかはわかりません。
推奨されるアプローチは、アプリにスレッドの状態に関するメッセージをテストに送信させることです。 OkHttp3IdlingResourceなどの組み込みメカニズムを使用できる場合があります。それ以外の場合は、テストのサポートのみを目的として、アプリのソースのさまざまな場所にコードを挿入する必要があります(アプリのロジックを知っておく必要があります!)。さらに、すべてのアニメーションをオフにする必要があります(ただし、UIの一部です)。
他のアプローチは待っています、例えばSystemClock.sleep(10000)。しかし、どれだけ長く待つかはわからず、長い遅延でも成功を保証することはできません。一方、テストは長く続きます。
私のアプローチは、時間条件を追加して相互作用を表示することです。例えば。 10000 mc(タイムアウト)の間に新しい画面が表示されることをテストします。しかし、私たちは待ちません(たとえば100ミリ秒ごと)必要なだけ早くチェックしますもちろん、テストスレッドをこのようにブロックしますが、通常、そのような場合に必要なのはまさにそれです。
Usage:
long timeout=10000;
long matchDelay=100; //(check every 100 ms)
EspressoExecutor myExecutor = new EspressoExecutor<ViewInteraction>(timeout, matchDelay);
ViewInteraction loginButton = onView(withId(R.id.login_btn));
loginButton.perform(click());
myExecutor.callForResult(()->onView(allOf(withId(R.id.title),isDisplayed())));
これは私のクラスソースです:
/**
* Created by alexshr on 02.05.2017.
*/
package com.skb.goodsapp;
import Android.os.SystemClock;
import Android.util.Log;
import Java.util.Date;
import Java.util.concurrent.Callable;
/**
* The utility repeats runnable or callable executing until it pass without errors or throws throwable after timeout.
* It works perfectly for Espresso tests.
* <p>
* Suppose the last view interaction (button click) activates some background threads (network, database etc.).
* As the result new screen should appear and we want to check it in our next step,
* but we don't know when new screen will be ready to be tested.
* <p>
* Recommended approach is to force your app to send messages about threads states to your test.
* Sometimes we can use built-in mechanisms like OkHttp3IdlingResource.
* In other cases you should insert code pieces in different places of your app sources (you should known app logic!) for testing support only.
* Moreover, we should turn off all your animations (although it's the part on ui).
* <p>
* The other approach is waiting, e.g. SystemClock.sleep(10000). But we don't known how long to wait and even long delays can't guarantee success.
* On the other hand your test will last long.
* <p>
* My approach is to add time condition to view interaction. E.g. we test that new screen should appear during 10000 mc (timeout).
* But we don't wait and check new screen as quickly as it appears.
* Of course, we block test thread such way, but usually it's just what we need in such cases.
* <p>
* Usage:
* <p>
* long timeout=10000;
* long matchDelay=100; //(check every 100 ms)
* EspressoExecutor myExecutor = new EspressoExecutor<ViewInteraction>(timeout, matchDelay);
* <p>
* ViewInteraction loginButton = onView(withId(R.id.login_btn));
* loginButton.perform(click());
* <p>
* myExecutor.callForResult(()->onView(allOf(withId(R.id.title),isDisplayed())));
*/
public class EspressoExecutor<T> {
private static String LOG = EspressoExecutor.class.getSimpleName();
public static long REPEAT_DELAY_DEFAULT = 100;
public static long BEFORE_DELAY_DEFAULT = 0;
private long mRepeatDelay;//delay between attempts
private long mBeforeDelay;//to start attempts after this initial delay only
private long mTimeout;//timeout for view interaction
private T mResult;
/**
* @param timeout timeout for view interaction
* @param repeatDelay - delay between executing attempts
* @param beforeDelay - to start executing attempts after this delay only
*/
public EspressoExecutor(long timeout, long repeatDelay, long beforeDelay) {
mRepeatDelay = repeatDelay;
mBeforeDelay = beforeDelay;
mTimeout = timeout;
Log.d(LOG, "created timeout=" + timeout + " repeatDelay=" + repeatDelay + " beforeDelay=" + beforeDelay);
}
public EspressoExecutor(long timeout, long repeatDelay) {
this(timeout, repeatDelay, BEFORE_DELAY_DEFAULT);
}
public EspressoExecutor(long timeout) {
this(timeout, REPEAT_DELAY_DEFAULT);
}
/**
* call with result
*
* @param callable
* @return callable result
* or throws RuntimeException (test failure)
*/
public T call(Callable<T> callable) {
call(callable, null);
return mResult;
}
/**
* call without result
*
* @param runnable
* @return void
* or throws RuntimeException (test failure)
*/
public void call(Runnable runnable) {
call(runnable, null);
}
private void call(Object obj, Long initialTime) {
try {
if (initialTime == null) {
initialTime = new Date().getTime();
Log.d(LOG, "sleep delay= " + mBeforeDelay);
SystemClock.sleep(mBeforeDelay);
}
if (obj instanceof Callable) {
Log.d(LOG, "call callable");
mResult = ((Callable<T>) obj).call();
} else {
Log.d(LOG, "call runnable");
((Runnable) obj).run();
}
} catch (Throwable e) {
long remain = new Date().getTime() - initialTime;
Log.d(LOG, "remain time= " + remain);
if (remain > mTimeout) {
throw new RuntimeException(e);
} else {
Log.d(LOG, "sleep delay= " + mRepeatDelay);
SystemClock.sleep(mRepeatDelay);
call(obj, initialTime);
}
}
}
}
https://Gist.github.com/alexshr/ca90212e49e74eb201fbc976255b47e
このためにアイドリングリソースを使用するのが最善だと思いますが( https://google.github.io/Android-testing-support-library/docs/espresso/idling-resource/ )、おそらく使用できますフォールバックとしてこれ:
/**
* Contains view interactions, view actions and view assertions which allow to set a timeout
* for finding a view and performing an action/view assertion on it.
* To be used instead of {@link Espresso}'s methods.
*
* @author Piotr Zawadzki
*/
public class TimeoutEspresso {
private static final int SLEEP_IN_A_LOOP_TIME = 50;
private static final long DEFAULT_TIMEOUT_IN_MILLIS = 10 * 1000L;
/**
* Use instead of {@link Espresso#onView(Matcher)}
* @param timeoutInMillis timeout after which an error is thrown
* @param viewMatcher view matcher to check for view
* @return view interaction
*/
public static TimedViewInteraction onViewWithTimeout(long timeoutInMillis, @NonNull final Matcher<View> viewMatcher) {
final long startTime = System.currentTimeMillis();
final long endTime = startTime + timeoutInMillis;
do {
try {
return new TimedViewInteraction(Espresso.onView(viewMatcher));
} catch (NoMatchingViewException ex) {
//ignore
}
SystemClock.sleep(SLEEP_IN_A_LOOP_TIME);
}
while (System.currentTimeMillis() < endTime);
// timeout happens
throw new PerformException.Builder()
.withCause(new TimeoutException("Timeout occurred when trying to find: " + viewMatcher.toString()))
.build();
}
/**
* Use instead of {@link Espresso#onView(Matcher)}.
* Same as {@link #onViewWithTimeout(long, Matcher)} but with the default timeout {@link #DEFAULT_TIMEOUT_IN_MILLIS}.
* @param viewMatcher view matcher to check for view
* @return view interaction
*/
public static TimedViewInteraction onViewWithTimeout(@NonNull final Matcher<View> viewMatcher) {
return onViewWithTimeout(DEFAULT_TIMEOUT_IN_MILLIS, viewMatcher);
}
/**
* A wrapper around {@link ViewInteraction} which allows to set timeouts for view actions and assertions.
*/
public static class TimedViewInteraction {
private ViewInteraction wrappedViewInteraction;
public TimedViewInteraction(ViewInteraction wrappedViewInteraction) {
this.wrappedViewInteraction = wrappedViewInteraction;
}
/**
* @see ViewInteraction#perform(ViewAction...)
*/
public TimedViewInteraction perform(final ViewAction... viewActions) {
wrappedViewInteraction.perform(viewActions);
return this;
}
/**
* {@link ViewInteraction#perform(ViewAction...)} with a timeout of {@link #DEFAULT_TIMEOUT_IN_MILLIS}.
* @see ViewInteraction#perform(ViewAction...)
*/
public TimedViewInteraction performWithTimeout(final ViewAction... viewActions) {
return performWithTimeout(DEFAULT_TIMEOUT_IN_MILLIS, viewActions);
}
/**
* {@link ViewInteraction#perform(ViewAction...)} with a timeout.
* @see ViewInteraction#perform(ViewAction...)
*/
public TimedViewInteraction performWithTimeout(long timeoutInMillis, final ViewAction... viewActions) {
final long startTime = System.currentTimeMillis();
final long endTime = startTime + timeoutInMillis;
do {
try {
return perform(viewActions);
} catch (RuntimeException ex) {
//ignore
}
SystemClock.sleep(SLEEP_IN_A_LOOP_TIME);
}
while (System.currentTimeMillis() < endTime);
// timeout happens
throw new PerformException.Builder()
.withCause(new TimeoutException("Timeout occurred when trying to perform view actions: " + viewActions))
.build();
}
/**
* @see ViewInteraction#withFailureHandler(FailureHandler)
*/
public TimedViewInteraction withFailureHandler(FailureHandler failureHandler) {
wrappedViewInteraction.withFailureHandler(failureHandler);
return this;
}
/**
* @see ViewInteraction#inRoot(Matcher)
*/
public TimedViewInteraction inRoot(Matcher<Root> rootMatcher) {
wrappedViewInteraction.inRoot(rootMatcher);
return this;
}
/**
* @see ViewInteraction#check(ViewAssertion)
*/
public TimedViewInteraction check(final ViewAssertion viewAssert) {
wrappedViewInteraction.check(viewAssert);
return this;
}
/**
* {@link ViewInteraction#check(ViewAssertion)} with a timeout of {@link #DEFAULT_TIMEOUT_IN_MILLIS}.
* @see ViewInteraction#check(ViewAssertion)
*/
public TimedViewInteraction checkWithTimeout(final ViewAssertion viewAssert) {
return checkWithTimeout(DEFAULT_TIMEOUT_IN_MILLIS, viewAssert);
}
/**
* {@link ViewInteraction#check(ViewAssertion)} with a timeout.
* @see ViewInteraction#check(ViewAssertion)
*/
public TimedViewInteraction checkWithTimeout(long timeoutInMillis, final ViewAssertion viewAssert) {
final long startTime = System.currentTimeMillis();
final long endTime = startTime + timeoutInMillis;
do {
try {
return check(viewAssert);
} catch (RuntimeException ex) {
//ignore
}
SystemClock.sleep(SLEEP_IN_A_LOOP_TIME);
}
while (System.currentTimeMillis() < endTime);
// timeout happens
throw new PerformException.Builder()
.withCause(new TimeoutException("Timeout occurred when trying to check: " + viewAssert.toString()))
.build();
}
}
}
次に、コード内で次のように呼び出します:
onViewWithTimeout(withId(R.id.button).perform(click());
の代わりに
onView(withId(R.id.button).perform(click());
これにより、ビューアクションおよびビューアサーションのタイムアウトを追加することもできます。
これを行う方法をミックスに追加します。
fun suspendUntilSuccess(actionToSucceed: () -> Unit, iteration : Int = 0) {
try {
actionToSucceed.invoke()
} catch (e: Throwable) {
Thread.sleep(200)
val incrementedIteration : Int = iteration + 1
if (incrementedIteration == 25) {
fail("Failed after waiting for action to succeed for 5 seconds.")
}
suspendUntilSuccess(actionToSucceed, incrementedIteration)
}
}
このように呼び出されます:
suspendUntilSuccess({
checkThat.viewIsVisible(R.id.textView)
})
最大反復、反復長などのパラメーターをsuspendUntilSuccess関数に追加できます。
私はまだアイドリングリソースを使用することを好みますが、たとえばデバイスのアニメーションが遅いためにテストが機能している場合は、この機能を使用します。もちろん、失敗する前に最大5秒間ハングする可能性があるため、成功するアクションが成功しない場合、テストの実行時間が長くなる可能性があります。