Tecnología de prueba automatizada de Android: el uso de Espresso

configuración

modificar la configuración

Primero habilite las opciones de desarrollador y luego, en las opciones de desarrollador, deshabilite las siguientes tres configuraciones:

  • Escalado de animación de ventana
  • escalado de animación de transición
  • Escalado de duración del animador

agregar dependencias

app/build.gradleAgregar dependencias al archivo

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

En app/build.gradleel archivo android.defaultConfigagregar

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

Nota: Las dependencias anteriores solo pueden lograr funciones básicas. Si desea utilizar todas las funciones, configure de la siguiente manera:

todas las dependencias

    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'

Los métodos llamados a continuación onView()son métodos estáticos y pueden ser import static XXXllamados directamente por Todos los métodos estáticos que deben importarse son los siguientes:

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;

componente API

Los componentes de API comúnmente utilizados incluyen:

  • Espresso: punto de entrada para interactuar con las vistas (a través de onView() y onData()). Además, se exponen las API como pressBack() que no están necesariamente asociadas con ninguna vista.
  • ViewMatchers: una colección de objetos que implementan la interfaz Matcher<?super View>. Puede pasar uno o más de estos objetos al método onView() para encontrar una vista en la jerarquía de vistas actual.
  • ViewActions: una colección de objetos ViewAction (como click() ) que se pueden pasar al método ViewInteraction.perform().
  • ViewAssertions: una colección de objetos ViewAssertion que se pueden pasar al método ViewInteraction.check(). En la mayoría de los casos, usará aserciones de coincidencias, que usan comparadores de vista para afirmar el estado de la vista actualmente seleccionada.

La mayoría de las instancias de Matcher, ViewAction y ViewAssertion disponibles son las siguientes (documentos oficiales de origen):
inserte la descripción de la imagen aquí
Instancias comunes de api pdf

usar

mando común

Ejemplo: MainActivityContiene un Buttony un TextView. Después de hacer clic en el botón, TextViewel contenido de cambiará a "Cambiado con éxito" .

Úselo Espressopara probar de la siguiente manera:

@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()El método se utiliza para obtener la vista actual coincidente. Tenga en cuenta que solo puede haber una vista coincidente; de ​​lo contrario, se informará un error.

withId()El método se utiliza para buscar vistas coincidentes, similares withText(), withHint()etc.

perform()El método se utiliza para realizar alguna acción, como hacer clic click(), presionar prolongadamente longClick(), hacer doble clicdoubleClick()

check()Se utiliza para aplicar aserciones a la vista actualmente seleccionada

matches()La aserción más utilizada, afirma el estado de la vista actualmente seleccionada. El ejemplo anterior es para afirmar si la Vista cuyo id es contenido coincide con la Vista cuyo texto es "cambio exitoso"

Controles relacionados con AdapterView

A diferencia de los controles normales, AdapterView(comúnmente ListView) solo se puede cargar un subconjunto de subvistas en la jerarquía de vista actual. Una búsqueda simple onView()no encontrará vistas que no estén cargadas actualmente. EspressoProporciona un onData()punto de entrada único que primero carga el elemento del adaptador relevante y lo enfoca antes de realizar operaciones en él o en cualquiera de sus elementos secundarios.

Ejemplo: abra Spinner, seleccione una entrada específica y luego verifique TextViewque esa entrada esté incluida. Spinnercreará un que contenga su contenido ListView, por lo que la necesidad deonData()

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

La siguiente figura muestra el diagrama de relación de herencia de AdapterView:
inserte la descripción de la imagen aquí

Advertencia: si la implementación personalizada de AdapterView viola el contrato de herencia, puede haber problemas al usar el método onData() (especialmente la API getItem()). En este caso, el mejor curso de acción es refactorizar el código de la aplicación. Si no puede hacer esto, puede implementar un AdapterViewProtocol personalizado coincidente.

Comparador personalizado y ViewAction

Antes de presentar RecyclerViewlas operaciones, echemos un vistazo a cómo personalizar Matchery ViewAction.

personalizarMatcher

Matcher<T>es una interfaz utilizada para hacer coincidir la vista, comúnmente se utilizan sus dos clases de implementación BoundedMatcher<T, S extends T>
yTypeSafeMatcher<T>

BoundedMatcher<T, S extends T>: Algo de azúcar sintáctico coincidente que le permite crear una coincidencia de un tipo dado mientras empareja solo elementos de proceso de un subtipo particular.
Parámetros de tipo:<T> - 匹配器的期望类型。<S> - T的亚型

TypeSafeMatcher<T>: implementa internamente verificaciones nulas, verifica el tipo y luego convierte

Ejemplo: Ingrese el valor EditText, si el valor comienza con , haga visible 000el TextView con el contenido "éxito" , de lo contrario, haga visible el TextView con el contenido " fallo ".


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

personalizarViewAction

Esto no es muy familiar, aquí hay una introducción a la implementación de la interfaz ViewAction, la función del método a implementar.

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

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

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

RecyclerView

RecyclerViewLos objetos funcionan AdapterViewde manera diferente a los objetos, por lo que no puede onData()interactuar con ellos utilizando los métodos.
Para interactuar Espressocon RecyclerView, puede usar espresso-contribel paquete, que tiene RecyclerViewActionsuna colección que define métodos para desplazarse a una posición o realizar acciones en elementos.

agregar dependencias

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

Los métodos de operación RecyclerViewson:

  • scrollTo(): ​​se desplaza a la vista correspondiente.
  • scrollToHolder(): se desplaza al titular de la vista coincidente.
  • scrollToPosition() - desplazarse a una posición específica.
  • actionOnHolderItem(): realiza una acción de vista en un titular de vista coincidente.
  • actionOnItem(): realiza una acción de vista en la vista coincidente.
  • actionOnItemAtPosition(): realiza una acción de vista en la vista en una posición específica.

Ejemplo: seleccione la función de eliminación: haga clic en Editar , el contenido de TextView se cambiará para eliminar y RecycleViewaparecerá una casilla de verificación para el elemento al mismo tiempo, marque el elemento que desea eliminar, haga clic en Eliminar , elimine el elemento especificado y RecycleViewel desaparecerá la casilla de verificación del elemento.

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

Nota: MatcherDataActionLlamado en assertThat(), no se recomienda este método. Aquí hay una prueba que no he encontrado una mejor manera de implementar.

Intención

Espresso-Intents es una extensión de Espresso, que admite la verificación y acumulación de Intents enviados por la aplicación bajo prueba.

Agregar dependencias:

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

Antes de escribir Espresso-Intentsuna prueba, es necesario configurarla IntentsTestRule. Esta es ActivityTestRuleuna extensión de la clase que le permite usar fácilmente la API en pruebas de interfaz funcional Espresso-Intents. IntentsTestRuleSe inicializará @Testantes de cada ejecución de prueba anotada Espresso-Intentsy se desasignará después de cada ejecución de prueba Espresso-Intents.

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

Verificar intención

Ejemplo: en EditText, ingrese un número de teléfono, haga clic en el botón de marcación y realice una llamada.

@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(): es Espresso-Intentsel método proporcionado para verificar la intención

Además, la intención también se puede verificar mediante una afirmación.

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

apostar

El método anterior puede resolver la operación de verificación de intención general, pero cuando necesitamos llamar startActivityForResult()al método para iniciar la cámara para obtener fotos, si usamos el método general, debemos hacer clic manualmente para tomar una foto, lo cual no se considera automático. prueba.

Espresso-IntentsSe proporciona un método intending()para resolver este problema proporcionando una respuesta de código auxiliar para una actividad iniciada con startActivityForResult(). En pocas palabras, no iniciará la cámara, sino que devolverá su propia intención definida.

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

recursos inactivos

Un recurso libre indica una operación asíncrona cuyos resultados afectan a las operaciones posteriores en la prueba de IU. EspressoEstas operaciones asincrónicas se pueden verificar de manera más confiable al probar su aplicación al registrar los recursos inactivos con .

agregar dependencias

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

Usemos el ejemplo oficial de Google para presentar cómo usar:

El primer paso: crear SimpleIdlingResourceuna clase para implementarIdlingResource

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

Paso 2: Cree una clase que realice tareas asincrónicasMessageDelayer

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

Paso 3: Inicie la tarea haciendo clic en el botón en 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;
    }
}

Paso 4: Crear casos de prueba

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

Insuficiente: Espressoproporciona un conjunto de funciones de sincronización avanzadas. Sin embargo, esta característica del marco solo se aplica a MessageQueuelas operaciones que publican mensajes en el , como Ver subclases que dibujan contenido en la pantalla.

otro

EspressoTambién hay multiproceso, WebView, verificación de accesibilidad, multiventana, etc. No estoy familiarizado con estos. Se recomienda leer la
documentación oficial de Android o los ejemplos oficiales a continuación.

ejemplo oficial

referencia

Supongo que te gusta

Origin blog.csdn.net/lichukuan/article/details/126861863
Recomendado
Clasificación