Espresso Testing Framework - Intents

O Android Intent é usado para abrir uma nova atividade, seja interna (abrir uma tela de detalhes do produto na tela da lista de produtos) ou externa (como abrir um discador para fazer uma chamada). A atividade de intenção interna é tratada de forma transparente pela estrutura de teste do espresso e não precisa de nenhum trabalho específico do lado do usuário. No entanto, invocar a atividade externa é realmente um desafio porque está fora do nosso escopo, o aplicativo em teste. Depois que o usuário invoca um aplicativo externo e sai do aplicativo em teste, as chances de o usuário voltar ao aplicativo com uma sequência de ação predefinida são menores. Portanto, precisamos assumir a ação do usuário antes de testar o aplicativo. O Espresso oferece duas opções para lidar com essa situação. Eles são os seguintes,

pretendido

Isso permite que o usuário certifique-se de que o intent correto seja aberto no aplicativo em teste.

pretendendo

Isso permite ao usuário simular uma atividade externa, como tirar uma foto da câmera, discar um número da lista de contatos, etc., e retornar ao aplicativo com um conjunto predefinido de valores (como uma imagem predefinida da câmera em vez da imagem real) .

Configuração

O Espresso oferece suporte à opção de intenção por meio de uma biblioteca de plug-ins e a biblioteca precisa ser configurada no arquivo gradle do aplicativo. A opção de configuração é a seguinte,

dependencies {
   // ...
   androidTestImplementation 'androidx.test.espresso:espresso-intents:3.1.1'
}

pretendido()

O plug-in de intent do Espresso fornece correspondências especiais para verificar se a intent invocada é a esperada. Os matchers fornecidos e a finalidade dos matchers são os seguintes,

hasAction

Isso aceita a ação do intent e retorna um matcher, que corresponde ao intent especificado.

hasData

Isso aceita os dados e retorna um matcher, que corresponde aos dados fornecidos para a intent ao invocá-la.

embalar

Isso aceita o nome do pacote do intent e retorna um matcher, que corresponde ao nome do pacote do intent invocado.

Agora, vamos criar um novo aplicativo e testá-lo quanto à atividade externa usando intend () para entender o conceito.

  • Inicie o Android Studio.

  • Crie um novo projeto conforme discutido anteriormente e nomeie-o IntentSampleApp.

  • Migre o aplicativo para a estrutura AndroidX usando Refactor → Migrar para o menu de opções AndroidX .

  • Crie uma caixa de texto, um botão para abrir a lista de contatos e outro para fazer uma chamada, alterando o activity_main.xml como mostrado abaixo,

<?xml version = "1.0" encoding = "utf-8"?>
<RelativeLayout xmlns:android = "http://schemas.android.com/apk/res/android"
   xmlns:app = "http://schemas.android.com/apk/res-auto"
   xmlns:tools = "http://schemas.android.com/tools"
   android:layout_width = "match_parent"
   android:layout_height = "match_parent"
   tools:context = ".MainActivity">
   <EditText
      android:id = "@+id/edit_text_phone_number"
      android:layout_width = "wrap_content"
      android:layout_height = "wrap_content"
      android:layout_centerHorizontal = "true"
      android:text = ""
      android:autofillHints = "@string/phone_number"/>
   <Button
      android:id = "@+id/call_contact_button"
      android:layout_width = "wrap_content"
      android:layout_height = "wrap_content"
      android:layout_centerHorizontal = "true"
      android:layout_below = "@id/edit_text_phone_number"
      android:text = "@string/call_contact"/>
   <Button
      android:id = "@+id/button"
      android:layout_width = "wrap_content"
      android:layout_height = "wrap_content"
      android:layout_centerHorizontal = "true"
      android:layout_below = "@id/call_contact_button"
      android:text = "@string/call"/>
</RelativeLayout>
  • Além disso, adicione o item abaixo no arquivo de recurso strings.xml ,

<string name = "phone_number">Phone number</string>
<string name = "call">Call</string>
<string name = "call_contact">Select from contact list</string>
  • Agora, adicione o código abaixo na atividade principal ( MainActivity.java ) sob o método onCreate .

public class MainActivity extends AppCompatActivity {
   @Override
   protected void onCreate(Bundle savedInstanceState) {
      // ... code
      // Find call from contact button
      Button contactButton = (Button) findViewById(R.id.call_contact_button);
      contactButton.setOnClickListener(new View.OnClickListener() {
         @Override
         public void onClick(View view) {
            // Uri uri = Uri.parse("content://contacts");
            Intent contactIntent = new Intent(Intent.ACTION_PICK,
               ContactsContract.Contacts.CONTENT_URI);
            contactIntent.setType(ContactsContract.CommonDataKinds.Phone.CONTENT_TYPE);
            startActivityForResult(contactIntent, REQUEST_CODE);
         }
      });
      // Find edit view
      final EditText phoneNumberEditView = (EditText)
         findViewById(R.id.edit_text_phone_number);
      // Find call button
      Button button = (Button) findViewById(R.id.button);
      button.setOnClickListener(new View.OnClickListener() {
         @Override
         public void onClick(View view) {
            if(phoneNumberEditView.getText() != null) {
               Uri number = Uri.parse("tel:" + phoneNumberEditView.getText());
               Intent callIntent = new Intent(Intent.ACTION_DIAL, number);
               startActivity(callIntent);
            }
         }
      });
   }
   // ... code
}

Aqui, programamos o botão com id, call_contact_button para abrir a lista de contatos e o botão com id, botão para discar a chamada.

  • Adicione uma variável estática REQUEST_CODE na classe MainActivity como mostrado abaixo,

public class MainActivity extends AppCompatActivity {
   // ...
   private static final int REQUEST_CODE = 1;
   // ...
}
  • Agora, adicione o método onActivityResult na classe MainActivity como abaixo,

public class MainActivity extends AppCompatActivity {
   // ...
   @Override
   protected void onActivityResult(int requestCode, int resultCode, Intent data) {
      if (requestCode == REQUEST_CODE) {
         if (resultCode == RESULT_OK) {
            // Bundle extras = data.getExtras();
            // String phoneNumber = extras.get("data").toString();
            Uri uri = data.getData();
            Log.e("ACT_RES", uri.toString());
            String[] projection = {
               ContactsContract.CommonDataKinds.Phone.NUMBER, 
               ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME };
            Cursor cursor = getContentResolver().query(uri, projection, null, null, null);
            cursor.moveToFirst();
            
            int numberColumnIndex =
               cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER);
            String number = cursor.getString(numberColumnIndex);
            
            int nameColumnIndex = cursor.getColumnIndex(
               ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME);
            String name = cursor.getString(nameColumnIndex);
            Log.d("MAIN_ACTIVITY", "Selected number : " + number +" , name : "+name);
            
            // Find edit view
            final EditText phoneNumberEditView = (EditText)
               findViewById(R.id.edit_text_phone_number);
            phoneNumberEditView.setText(number);
         }
      }
   };
   // ...
}

Aqui, onActivityResult será chamado quando um usuário retornar ao aplicativo após abrir a lista de contatos usando o botão call_contact_button e selecionar um contato. Depois que o método onActivityResult é invocado, ele obtém o contato selecionado pelo usuário, localiza o número do contato e o define na caixa de texto.

  • Execute o aplicativo e verifique se está tudo bem. A aparência final do aplicativo de amostra Intent é mostrada abaixo,

  • Agora, configure a intenção do espresso no arquivo gradle do aplicativo, conforme mostrado abaixo,

dependencies {
   // ...
   androidTestImplementation 'androidx.test.espresso:espresso-intents:3.1.1'
}
  • Clique na opção de menu Sincronizar agora fornecida pelo Android Studio. Isso fará o download da biblioteca de teste de intenção e a configurará corretamente.

  • Abra o arquivo ExampleInstrumentedTest.java e adicione o IntentsTestRule em vez do AndroidTestRule normalmente usado . IntentTestRule é uma regra especial para lidar com testes de intenção.

public class ExampleInstrumentedTest {
   // ... code
   @Rule
   public IntentsTestRule<MainActivity> mActivityRule =
   new IntentsTestRule<>(MainActivity.class);
   // ... code
}
  • Adicione duas variáveis ​​locais para definir o número de telefone de teste e o nome do pacote do discador conforme abaixo,

public class ExampleInstrumentedTest {
   // ... code
   private static final String PHONE_NUMBER = "1 234-567-890";
   private static final String DIALER_PACKAGE_NAME = "com.google.android.dialer";
   // ... code
}
  • Corrija os problemas de importação usando a opção Alt + Enter fornecida pelo android studio ou inclua as instruções de importação abaixo,

import android.content.Context;
import android.content.Intent;

import androidx.test.InstrumentationRegistry;
import androidx.test.espresso.intent.rule.IntentsTestRule;
import androidx.test.runner.AndroidJUnit4;

import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard;
import static androidx.test.espresso.action.ViewActions.typeText;
import static androidx.test.espresso.intent.Intents.intended;
import static androidx.test.espresso.intent.matcher.IntentMatchers.hasAction;
import static androidx.test.espresso.intent.matcher.IntentMatchers.hasData;
import static androidx.test.espresso.intent.matcher.IntentMatchers.toPackage;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static org.hamcrest.core.AllOf.allOf;
import static org.junit.Assert.*;
  • Adicione o caso de teste abaixo para testar se o discador é chamado corretamente,

public class ExampleInstrumentedTest {
   // ... code
   @Test
   public void validateIntentTest() {
      onView(withId(R.id.edit_text_phone_number))
         .perform(typeText(PHONE_NUMBER), closeSoftKeyboard());
      onView(withId(R.id.button)) .perform(click());
      intended(allOf(
         hasAction(Intent.ACTION_DIAL),
         hasData("tel:" + PHONE_NUMBER),
         toPackage(DIALER_PACKAGE_NAME)));
   }
   // ... code
}

Aqui, os matchers hasAction , hasData e toPackage são usados ​​junto com o matcher allOf para obter sucesso apenas se todos os matchers forem passados.

  • Agora, execute o ExampleInstrumentedTest por meio do menu de conteúdo no Android Studio.

pretendendo ()

O Espresso fornece um método especial - intending () para simular uma ação de intenção externa. intending () aceita o nome do pacote do intent a ser simulado e fornece um método respondWith para definir como o intent simulado precisa ser respondido conforme especificado abaixo,

intending(toPackage("com.android.contacts")).respondWith(result);

Aqui, respondWith () aceita o resultado da intenção do tipo Instrumentation.ActivityResult . Podemos criar um novo intent de stub e definir manualmente o resultado conforme especificado abaixo,

// Stub intent
Intent intent = new Intent();
intent.setData(Uri.parse("content://com.android.contacts/data/1"));
Instrumentation.ActivityResult result =
   new Instrumentation.ActivityResult(Activity.RESULT_OK, intent);

O código completo para testar se um aplicativo de contato está aberto corretamente é o seguinte,

@Test
public void stubIntentTest() {
   // Stub intent
   Intent intent = new Intent();
   intent.setData(Uri.parse("content://com.android.contacts/data/1"));
   Instrumentation.ActivityResult result =
      new Instrumentation.ActivityResult(Activity.RESULT_OK, intent);
   intending(toPackage("com.android.contacts")).respondWith(result);
   
   // find the button and perform click action
   onView(withId(R.id.call_contact_button)).perform(click());
   
   // get context
   Context targetContext2 = InstrumentationRegistry.getInstrumentation().getTargetContext();
   
   // get phone number
   String[] projection = { ContactsContract.CommonDataKinds.Phone.NUMBER,
      ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME };
   Cursor cursor =
      targetContext2.getContentResolver().query(Uri.parse("content://com.android.cont
      acts/data/1"), projection, null, null, null);
   
   cursor.moveToFirst();
   int numberColumnIndex =
      cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER);
   String number = cursor.getString(numberColumnIndex);
   
   // now, check the data
   onView(withId(R.id.edit_text_phone_number))
   .check(matches(withText(number)));
}

Aqui, criamos um novo intent e definimos o valor de retorno (ao invocar o intent) como a primeira entrada da lista de contatos, content: //com.android.contacts/data/1 . Em seguida, definimos o método de intenção para simular a intenção recém-criada no lugar da lista de contatos. Ele define e chama nosso intent recém-criado quando o pacote, com.android.contacts , é invocado e a primeira entrada padrão da lista é retornada. Em seguida, disparamos a ação click () para iniciar o intent simulado e, por fim, verificamos se o número de telefone de invocar o intent simulado e o número da primeira entrada na lista de contatos são iguais.

Se houver algum problema de importação ausente, corrija esses problemas de importação usando a opção Alt + Enter fornecida pelo android studio ou inclua as instruções de importação abaixo,

import android.app.Activity;
import android.app.Instrumentation;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.provider.ContactsContract;

import androidx.test.InstrumentationRegistry;
import androidx.test.espresso.ViewInteraction;
import androidx.test.espresso.intent.rule.IntentsTestRule;
import androidx.test.runner.AndroidJUnit4;

import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard;
import static androidx.test.espresso.action.ViewActions.typeText;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.intent.Intents.intended;
import static androidx.test.espresso.intent.Intents.intending;
import static androidx.test.espresso.intent.matcher.IntentMatchers.hasAction;
import static androidx.test.espresso.intent.matcher.IntentMatchers.hasData;
import static androidx.test.espresso.intent.matcher.IntentMatchers.toPackage;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.core.AllOf.allOf;
import static org.junit.Assert.*;

Adicione a regra abaixo na classe de teste para fornecer permissão para ler a lista de contatos -

@Rule
public GrantPermissionRule permissionRule =
GrantPermissionRule.grant(Manifest.permission.READ_CONTACTS);

Adicione a opção abaixo no arquivo de manifesto do aplicativo, AndroidManifest.xml -

<uses-permission android:name = "android.permission.READ_CONTACTS" />

Agora, certifique-se de que a lista de contatos tenha pelo menos uma entrada e execute o teste usando o menu de contexto do Android Studio.