Android 自動テスト テクノロジー - Espresso の使用

構成

設定を変更する

まず開発者向けオプションを有効にし、次に開発者向けオプションで次の 3 つの設定を無効にします。

  • ウィンドウアニメーションのスケーリング
  • トランジションアニメーションのスケーリング
  • アニメーターの継続時間のスケーリング

依存関係を追加する

依存関係をファイルapp/build.gradleに追加する

androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test:rules:1.2.0'

app/build.gradleファイルandroid.defaultConfig追加

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

注: 上記の依存関係は基本的な機能のみを実現します。すべての機能を使用したい場合は、次のように設定してください。

すべての依存関係

    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test.ext:truth:1.2.0'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
    androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.2.0'
    androidTestImplementation 'androidx.test:runner:1.2.0'
    androidTestImplementation 'androidx.test:rules:1.2.0'
    androidTestImplementation 'androidx.test.espresso:espresso-intents:3.2.0'
    implementation 'androidx.recyclerview:recyclerview:1.1.0'
    implementation 'androidx.test.espresso:espresso-idling-resource:3.2.0'

以下で呼び出されるメソッドはonView()静的メソッドであり、 によって直接呼び出すことができますimport static XXX。インポートする必要があるすべての静的メソッドは次のとおりです。

import static androidx.test.espresso.Espresso.*;
import static androidx.test.espresso.action.ViewActions.*;
import static androidx.test.espresso.assertion.ViewAssertions.*;
import static androidx.test.espresso.intent.Intents.intended;
import static androidx.test.espresso.intent.Intents.intending;
import static androidx.test.espresso.intent.matcher.ComponentNameMatchers.*;
import static androidx.test.espresso.intent.matcher.IntentMatchers.*;
import static androidx.test.espresso.matcher.ViewMatchers.*;
import static androidx.test.ext.truth.content.IntentSubject.assertThat;

APIコンポーネント

一般的に使用される API コンポーネントには次のものがあります。

  • Espresso - ビューと対話するためのエントリ ポイント (onView() および onData() 経由)。さらに、必ずしもビューに関連付けられていない pressBack() などの API が公開されます。
  • ViewMatchers - Matcher<? super View> インターフェイスを実装するオブジェクトのコレクション。これらのオブジェクトの 1 つ以上を onView() メソッドに渡して、現在のビュー階層でビューを検索できます。
  • ViewActions - ViewInteraction.perform() メソッドに渡すことができる ViewAction オブジェクト ( click() など) のコレクション。
  • ViewAssertions - ViewInteraction.check() メソッドに渡すことができる ViewAssertion オブジェクトのコレクション。ほとんどの場合、ビュー マッチャーを使用して現在選択されているビューの状態をアサートする一致アサーションを使用します。

利用可能な Matcher、ViewAction、ViewAssertion インスタンスのほとんどは次のとおりです (ソース公式ドキュメント):
ここに画像の説明を挿入
Common API インスタンス pdf

使用

共通制御

例: MainActivityaButtonと aが含まれますTextViewボタンをクリックすると、TextViewの内容が「正常に変更されました」に変わります。

次のようにテストするために使用しますEspresso

@RunWith(AndroidJUnit4.class)
@LargeTest
public class ChangeTextTest {
    
    
   @Rule
    public ActivityTestRule<MainActivity> activityRule =
            new ActivityTestRule<>(MainActivity.class);
    @Test
    public void test_change_text(){
    
    
        onView(withId(R.id.change))
                .perform(click());
        onView(withId(R.id.content))
              .check(matches(withText("改变成功")));
    }
}    

onView()このメソッドは、一致する現在のビューを取得するために使用されます。一致するビューは 1 つだけであることに注意してください。そうでない場合は、エラーが報告されます。

withId()このメソッドは、同様にwithText()一致するビューを検索するために使用されます。withHint()

perform()click()メソッドは、クリック、長押しlongClick()、ダブルクリックなどのアクションを実行するために使用されます。doubleClick()

check()現在選択されているビューにアサーションを適用するために使用されます

matches()最も一般的に使用されるアサーションで、現在選択されているビューの状態をアサートします。上記の例は、ID が content であるビューがテキストが「変更成功」であるビューと一致するかどうかをアサートすることです。

AdaptorView 関連のコントロール

通常のコントロールとは異なり、AdapterView(通常はListView) サブビューのサブセットのみを現在のビュー階層にロードできます。単純なonView()検索では、現在ロードされていないビューは見つかりません。最初に関連するアダプター項目をロードし、その項目またはその子に対する操作を実行する前にその項目にフォーカスを置く、Espresso単一のエントリー・ポイントを提供します。onData()

例: を開きSpinner、特定のエントリを選択し、TextViewそのエントリが含まれていることを確認します。Spinnerは、そのコンテンツを含む を作成しますListView。そのため、onData()

@RunWith(AndroidJUnit4.class)
@LargeTest
public class SpinnerTest {
    
    
   @Rule
    public ActivityTestRule<MainActivity> activityRule =
            new ActivityTestRule<>(MainActivity.class);
    @Test
    public void test_spinner(){
    
    
        String content = "学校";
        //点击Spnner,显示项目
        onView(withId(R.id.change)).perform(click());
        //点击指定的内容
        onData(allOf(is(instanceOf(String.class)), is(content))).perform(click());
        //判断TextView是否包含指定内容
        onView(withId(R.id.content))
                .check(matches(withText(containsString(content))));
    }
}

次の図は、AdapterView の継承関係図を示しています。
ここに画像の説明を挿入

警告: AdaptorView のカスタム実装が継承契約に違反している場合、onData() メソッド (特に getItem() API) の使用時に問題が発生する可能性があります。この場合、最善の対処法はアプリケーション コードをリファクタリングすることです。これができない場合は、一致するカスタム AdaptorViewProtocol を実装できます。

カスタムマッチャーとViewAction

RecyclerView操作を紹介する前に、Matcherと のカスタマイズ方法を見てみましょうViewAction

カスタマイズMatcher

Matcher<T>はビューと一致させるために使用されるインターフェイスであり、一般的に使用されるのはその 2 つの実装クラスBoundedMatcher<T, S extends T>
TypeSafeMatcher<T>

BoundedMatcher<T, S extends T>: 特定のサブタイプの処理項目のみを照合しながら、特定のタイプの照合を作成できるようにする一部の照合構文シュガー。
型パラメータ:<T> - 匹配器的期望类型。<S> - T的亚型

TypeSafeMatcher<T>: 内部的に null チェックを実装し、型をチェックして変換します。

例: EditText 値を入力します。値が で始まる場合は、コンテンツ「success」000の TextViewが表示されます。それ以外の場合は、コンテンツ「 failure 」の TextView が表示されます。


@RunWith(AndroidJUnit4.class)
@LargeTest
public class EditTextTest {
    
    
   @Rule
    public ActivityTestRule<MainActivity> activityRule =
            new ActivityTestRule<>(MainActivity.class);

   @Test
    public void rightInput() {
    
    
        onView(withId(R.id.editText))
                .check(matches(EditMatcher.isRight()))
                .perform(typeText("000123"), ViewActions.closeSoftKeyboard());
        onView(withId(R.id.button)).perform(click());
        onView(withId(R.id.textView_success)).check(matches(isDisplayed()));
        onView(withId(R.id.textView_fail)).check(matches(not(isDisplayed())));
    }

    @Test
    public void errorInput() {
    
    
        onView(withId(R.id.editText))
                .check(matches(EditMatcher.isRight()))
                .perform(typeText("003"), ViewActions.closeSoftKeyboard());
        onView(withId(R.id.button)).perform(click());
        onView(withId(R.id.textView_success)).check(matches(not(isDisplayed())));
        onView(withId(R.id.textView_fail)).check(matches(isDisplayed()));
    }
       
   static class EditMatcher{
    
    
       static Matcher<View> isRight(){
    
    
           //自定义Matcher
           return new BoundedMatcher<View, EditText>(EditText.class) {
    
    
               @Override
               public void describeTo(Description description) {
    
    
                     description.appendText("EditText不满足要求");
               }

               @Override
               protected boolean matchesSafely(EditText item) {
    
    
                  //在输入EditText之前,先判EditText是否可见以及hint是否为指定值
                   if (item.getVisibility() == View.VISIBLE &&
                   item.getText().toString().isEmpty())
                       return true;
                   else
                   return false;
               }
           };
       }
   }
}   

カスタマイズViewAction

あまり馴染みのない話ですが、ViewAction インターフェースの実装、実装するメソッドの機能を紹介します。

 /**
   *符合某种限制的视图
   */
  public Matcher<View> getConstraints();

  /**
   *返回视图操作的描述。 *说明不应该过长,应该很好地适应于一句话
   */
  public String getDescription();

  /**
   * 执行给定的视图这个动作。
   *PARAMS:uiController - 控制器使用与UI交互。
   *view - 在采取行动的view。 不能为null
   */
  public void perform(UiController uiController, View view);
}

リサイクラービュー

RecyclerViewオブジェクトはオブジェクトとは動作AdapterViewが異なるため、メソッドをonData()使用してオブジェクトを操作することはできません。と対話する
には、位置までスクロールしたり項目に対してアクションを実行したりするためのメソッドを定義するコレクションを含むパッケージを使用できますEspressoRecyclerViewespresso-contribRecyclerViewActions

依存関係を追加する

androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.2.0'

操作方法はRecyclerView次のとおりです。

  • scrollTo() - 一致するビューまでスクロールします。
  • scrollToHolder() - 一致するビュー ホルダーまでスクロールします。
  • scrollToPosition() - 特定の位置までスクロールします。
  • actionOnHolderItem() - 一致するビュー ホルダーに対してビュー アクションを実行します。
  • actionOnItem() - 一致するビューに対してビュー アクションを実行します。
  • actionOnItemAtPosition() - ビューの特定の位置でビュー アクションを実行します。

例: 削除機能を選択します。[編集]をクリックすると、TextView の内容が削除に変更され、同時にRecycleView項目のチェック ボックスが表示されます。 削除する項目にチェックを入れ、 [削除]をクリックし、指定した項目を削除します。RecycleView項目のチェックボックスが消えます

@RunWith(AndroidJUnit4.class)
@LargeTest
public class RecyclerViewTest {
    
    
   @Rule
    public ActivityTestRule<RecyclerActivity> activityRule =
            new ActivityTestRule<>(RecyclerActivity.class);
   

    static class ClickCheckBoxAction implements ViewAction{
    
    
        
        @Override
        public Matcher<View> getConstraints() {
    
    
            return any(View.class);
        }

        @Override
        public String getDescription() {
    
    
            return null;
        }

        @Override
        public void perform(UiController uiController, View view) {
    
    
            CheckBox box = view.findViewById(R.id.checkbox);
            box.performClick();//点击
        }
    }
    
    static class MatcherDataAction implements ViewAction{
    
    
        
        private String require;

        public MatcherDataAction(String require) {
    
    
            this.require = require;
        }

        @Override
        public Matcher<View> getConstraints() {
    
    
            return any(View.class);
        }

        @Override
        public String getDescription() {
    
    
            return null;
        }

        @Override
        public void perform(UiController uiController, View view) {
    
    
            TextView text = view.findViewById(R.id.text);
            assertThat("数据值不匹配",require,equalTo(text.getText().toString()));
        }
    }
    
    public void delete_require_data(){
    
    
        //获取RecyclerView中显示的所有数据
        List<String> l = new ArrayList<>(activityRule.getActivity().getData());
        //点击 编辑 ,判断text是否变成 删除
        onView(withId(R.id.edit))
                .perform(click())
                .check(matches(withText("删除")));
        //用来记录要删除的项,
        Random random = new Random();
        int time = random.nextInt(COUNT);
        List<String> data = new ArrayList<>(COUNT);
        for (int i = 0; i < COUNT; i++) {
    
    
            data.add("");
        }
        for (int i = 0; i < time; i++) {
    
    
            //随机生成要删除的位置
            int position = random.nextInt(COUNT);
            //由于再次点击会取消,这里用来记录最后确定要删除的项
            if (data.get(position).equals(""))
                data.set(position,"测试数据"+position);
            else data.set(position,"");
            //调用RecyclerViewActions.actionOnItemAtPosition()方法,滑到指定位置
            //在执行指定操作
           onView(withId(R.id.recycler)).
                  perform(RecyclerViewActions.actionOnItemAtPosition(position,new ClickCheckBoxAction()));
        }
        //点击 删除 ,判断text是否变成 编辑
        onView(withId(R.id.edit))
                .perform(click(),doubleClick())
                .check(matches(withText("编辑")));
        //删除无用的项
        data.removeIf(s -> s.equals(""));
        //获取最后保存的项
        l.removeAll(data);
        //依次判断保留的项是否还存在
        for (int i = 0; i < l.size(); i++) {
    
    
            final String require = l.get(i);
            onView(withId(R.id.recycler))
                    .perform(RecyclerViewActions.
                            actionOnItemAtPosition(i,new MatcherDataAction(require)));
        }
    }
}

注:MatcherDataActionで呼び出されるassertThat()このメソッドは推奨されません。これは、私が実装するより良い方法を見つけられなかったテストです。

意図

Espresso-Intents は Espresso の拡張機能であり、テスト対象のアプリケーションによって送信されるインテントの検証と蓄積をサポートします。

依存関係を追加します。

androidTestImplementation 'androidx.test.ext:truth:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.2.0'

テストを作成する前にEspresso-Intents、テストをセットアップする必要がありますIntentsTestRuleこれは、ActivityTestRule機能インターフェイス テストで API を簡単に使用できるようにするクラスの拡張機能ですEspresso-Intents注釈付きの各テストの実行前に初期化されIntentsTestRule各テストの実行後に割り当てが解除されます@Test Espresso-IntentsEspresso-Intents

 @Rule
 public IntentsTestRule<MainActivity> mActivityRule = new IntentsTestRule<>(
            MainActivity.class);

意図の検証

例: EditText で電話番号を入力し、ダイヤル ボタンをクリックして電話をかけます。

@RunWith(AndroidJUnit4.class)
@LargeTest
public class IntentTest {
    
    
   
    //设置拨打电话的权限的环境
    @Rule
    public GrantPermissionRule grantPermissionRule = GrantPermissionRule.grant("android.permission.CALL_PHONE");

    @Rule
    public IntentsTestRule<MainActivity> mActivityRule = new IntentsTestRule<>(
            MainActivity.class);

    @Test
    public void test_start_other_app_intent(){
    
    
         String phoneNumber = "123456";
         //输入电话号码
         onView(withId(R.id.phone))
                 .perform(typeText(phoneNumber), ViewActions.closeSoftKeyboard());
        //点击拨打
         onView(withId(R.id.button))
                 .perform(click());
         //验证Intent是否正确
         intended(allOf(
                hasAction(Intent.ACTION_CALL),
                hasData(Uri.parse("tel:"+phoneNumber))));
    }
}

intended()Espresso-Intents:意図を検証するために提供されるメソッドです

さらに、アサーションによって意図を検証することもできます

Intent receivedIntent = Iterables.getOnlyElement(Intents.getIntents());
assertThat(receivedIntent)
      .extras()
      .string("phone")
      .isEqualTo(phoneNumber);

賭け金

上記の方法で一般的なインテント検証操作は解決できますが、startActivityForResult()写真を取得するためにカメラを起動するメソッドを呼び出す必要がある場合、一般的な方法を使用すると、手動でクリックして写真を撮る必要があり、これは自動化されたものとはみなされません。テスト。

Espresso-Intentsintending()startActivityForResult() で開始されたアクティビティにスタブ応答を提供することで、この問題を解決するメソッドが提供されています。簡単に言えば、カメラは起動しませんが、独自に定義したインテントを返します。

@RunWith(AndroidJUnit4.class)
@LargeTest
public class TakePictureTest {
    
    
       
        public static BoundedMatcher<View, ImageView> hasDrawable() {
    
    
            return new BoundedMatcher<View, ImageView>(ImageView.class) {
    
    
                @Override
                public void describeTo(Description description) {
    
    
                    description.appendText("has drawable");
                }

                @Override
                public boolean matchesSafely(ImageView imageView) {
    
    
                    return imageView.getDrawable() != null;
                }
            };
        }
        
    @Rule
    public IntentsTestRule<MainActivity> mIntentsRule = new IntentsTestRule<>(
            MainActivity.class);

    @Rule
    public GrantPermissionRule grantPermissionRule = GrantPermissionRule.grant(Manifest.permission.CAMERA);

    @Before
    public void stubCameraIntent() {
    
    
        Instrumentation.ActivityResult result = createImageCaptureActivityResultStub();
        intending(hasAction(MediaStore.ACTION_IMAGE_CAPTURE)).respondWith(result);
    }

    @Test
    public void takePhoto_drawableIsApplied() {
    
    
        //先检查ImageView中是否已经设置了图片
        onView(withId(R.id.image)).check(matches(not(hasDrawable())));
        // 点击拍照
        onView(withId(R.id.button)).perform(click());
        // 判断ImageView中是否已经设置了图片
        onView(withId(R.id.image)).check(matches(hasDrawable()));
    }

    private Instrumentation.ActivityResult createImageCaptureActivityResultStub() {
    
    
        //自己定义Intent
        Bundle bundle = new Bundle();
        bundle.putParcelable("data", BitmapFactory.decodeResource(
                mIntentsRule.getActivity().getResources(), R.drawable.ic_launcher_round));
        Intent resultData = new Intent();
        resultData.putExtras(bundle);
        return new Instrumentation.ActivityResult(Activity.RESULT_OK, resultData);
    }
}

アイドル状態のリソース

空きリソースは、結果が UI テストの後続の操作に影響を与える非同期操作を示します。Espressoこれらの非同期操作は、アプリケーションをテストするときにアイドル状態のリソースを に登録することでより確実に検証できます。

依存関係を追加する

implementation 'androidx.test.espresso:espresso-idling-resource:3.2.0'

Google の公式例を使用して、使用方法を紹介します。

最初のステップ:SimpleIdlingResource実装するクラスを作成するIdlingResource

public class SimpleIdlingResource implements IdlingResource {
    
    

    @Nullable
    private volatile ResourceCallback mCallback;

    private AtomicBoolean mIsIdleNow = new AtomicBoolean(true);

    @Override
    public String getName() {
    
    
        return this.getClass().getName();
    }

    /**
     *false 表示这里有正在进行的任务,而true表示异步任务完成
     */
    @Override
    public boolean isIdleNow() {
    
    
        return mIsIdleNow.get();
    }

    @Override
    public void registerIdleTransitionCallback(ResourceCallback callback) {
    
    
        mCallback = callback;
    }

    public void setIdleState(boolean isIdleNow) {
    
    
        mIsIdleNow.set(isIdleNow);
        if (isIdleNow && mCallback != null) {
    
    
           //调用这个方法后,Espresso不会再检查isIdleNow()的状态,直接判断异步任务完成
            mCallback.onTransitionToIdle();
        }
    }
}

ステップ 2: 非同期タスクを実行するクラスを作成するMessageDelayer

class MessageDelayer {
    
    

    private static final int DELAY_MILLIS = 3000;

    interface DelayerCallback {
    
    
        void onDone(String text);
    }

    static void processMessage(final String message, final DelayerCallback callback,
                               @Nullable final SimpleIdlingResource idlingResource) {
    
    
        if (idlingResource != null) {
    
    
            idlingResource.setIdleState(false);
        }
        Handler handler = new Handler();
        new Thread(()->{
    
    
            try {
    
    
                Thread.sleep(DELAY_MILLIS);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            handler.post(new Runnable() {
    
    
                @Override
                public void run() {
    
    
                    if (callback != null) {
    
    
                        callback.onDone(message);
                        if (idlingResource != null) {
    
    
                            idlingResource.setIdleState(true);
                        }
                    }
                }
            });
        }).start();
    }
}

ステップ 3: MainActivity のボタンをクリックしてタスクを開始する

public class MainActivity extends AppCompatActivity implements View.OnClickListener,
        MessageDelayer.DelayerCallback {
    
    

    private TextView mTextView;
    private EditText mEditText;

    @Nullable
    private SimpleIdlingResource mIdlingResource;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        findViewById(R.id.changeTextBt).setOnClickListener(this);
        mTextView = findViewById(R.id.textToBeChanged);
        mEditText = findViewById(R.id.editTextUserInput);
    }

    @Override
    public void onClick(View view) {
    
    
        final String text = mEditText.getText().toString();
        if (view.getId() == R.id.changeTextBt) {
    
    
            mTextView.setText("正在等待");
            MessageDelayer.processMessage(text, this, mIdlingResource);
        }
    }

    @Override
    public void onDone(String text) {
    
    
        mTextView.setText(text);
    }

    /**
     * 仅测试能调用,创建并返回新的SimpleIdlingResource
     */
    @VisibleForTesting
    @NonNull
    public IdlingResource getIdlingResource() {
    
    
        if (mIdlingResource == null) {
    
    
            mIdlingResource = new SimpleIdlingResource();
        }
        return mIdlingResource;
    }
}

ステップ 4: テスト ケースを作成する

@RunWith(AndroidJUnit4.class)
@LargeTest
public class ChangeTextBehaviorTest {
    
    

    private static final String STRING_TO_BE_TYPED = "Espresso";

    private IdlingResource mIdlingResource;


    /**
     *注册IdlingResource实例
     */
    @Before
    public void registerIdlingResource() {
    
    
        ActivityScenario activityScenario = ActivityScenario.launch(MainActivity.class);
        activityScenario.onActivity((ActivityScenario.ActivityAction<MainActivity>) activity -> {
    
    
            mIdlingResource = activity.getIdlingResource();
            IdlingRegistry.getInstance().register(mIdlingResource);
        });
    }

    @Test
    public void changeText_sameActivity() {
    
    
        onView(withId(R.id.editTextUserInput))
                .perform(typeText(STRING_TO_BE_TYPED), closeSoftKeyboard());
        onView(withId(R.id.changeTextBt)).perform(click());
        //只需要注册IdlingResource实例,Espresso就会自动在这里等待,直到异步任务完成
        //在执行下面的代码
        onView(withId(R.id.textToBeChanged)).check(matches(withText(STRING_TO_BE_TYPED)));
    }
    //取消注册
    @After
    public void unregisterIdlingResource() {
    
    
        if (mIdlingResource != null) {
    
    
            IdlingRegistry.getInstance().unregister(mIdlingResource);
        }
    }
}

不十分:Espresso一連の高度な同期機能を提供します。ただし、フレームワークのこの機能は、MessageQueueコンテンツを画面に描画する View サブクラスなど、 にメッセージをパブリッシュする操作にのみ適用されます。

他の

Espresso他にもマルチプロセス、WebView、アクセシビリティチェック、マルチウィンドウなどもありますが、よく分からないので
Androidの公式ドキュメントや以下の公式サンプルを読むことをお勧めします。

公式の例

  • IntentsBasicSample : 意図した() と意図した() の基本的な使用法。
  • IdlingResourceSample : バックグラウンド ジョブと同期されます。
  • BasicSample : 基本的な Espresso サンプル。
  • CustomMatcherSample : EditText オブジェクトのヒント プロパティに一致するように Espresso を拡張する方法を示します。
  • DataAdapterSample : Espresso の List オブジェクトと AdaptorView オブジェクトの onData() エントリ ポイントを示します。
  • IntentsAdvancedSample : カメラを使用してビットマップを取得するユーザーをシミュレートします。
  • MultiWindowSample : Espresso を別のウィンドウにポイントする方法を示します。
  • RecyclerViewSample : Espresso の RecyclerView アクション。
  • WebBasicSample : Espresso-Web を使用して WebView オブジェクトと対話します。

参考

おすすめ

転載: blog.csdn.net/lichukuan/article/details/126861863