Exemplo de testes de autoinstrumentação

Quando um teste de instrumentação é iniciado, o pacote de destino é reiniciado com o código de instrumentação injetado e iniciado para execução. Uma exceção é que o pacote de destino aqui não pode ser o framework do aplicativo Android em si, como o pacote android, porque isso leva à situação paradoxal em que o framework do Android precisaria ser reiniciado, o que é o que oferece suporte às funções do sistema, incluindo a própria instrumentação.

Isso significa que um teste de instrumentação não pode se injetar no framework do Android, também conhecido como servidor do sistema, para execução. Para testar o framework do Android, o código de teste pode invocar apenas superfícies de API públicas ou aquelas expostas usando a linguagem de definição de interface do Android AIDL (link em inglês) disponível na árvore de origem da plataforma. Para essa categoria de testes, não faz sentido segmentar um pacote específico. Portanto, é comum que essas instrumentações sejam declaradas para segmentar o próprio pacote de aplicativos de teste, conforme definido na tag <manifest> de AndroidManifest.xml.

Dependendo dos requisitos, os pacotes de aplicativos de teste nessa categoria também podem:

  • Agrupe as atividades necessárias para o teste.
  • Compartilhe o ID do usuário com o sistema.
  • Ser assinado com a chave da plataforma.
  • Ser compilado com base na origem do framework, e não no SDK público.

Essa categoria de testes de instrumentação às vezes é chamada de auto-instrumentação. Confira alguns exemplos de testes de autoinstrumentação na fonte da plataforma:

O exemplo abordado aqui é a criação de um novo teste de instrumentação com o pacote de destino definido no próprio pacote de aplicativo de teste. Neste guia, usamos o seguinte teste como exemplo:

Recomendamos que você navegue pelo código primeiro para ter uma ideia geral antes de continuar.

Decidir um local de origem

Normalmente, sua equipe já tem um padrão estabelecido de lugares para verificar no código e lugares para adicionar testes. A maioria das equipes tem um único repositório git ou compartilha um com outras equipes, mas tem um subdiretório dedicado que contém o código-fonte do componente.

Supondo que o local raiz da origem do componente seja <component source root>, a maioria dos componentes tem pastas src e tests abaixo dele, além de alguns arquivos adicionais, como Android.mk (ou divididos em outros arquivos .mk), o arquivo de manifesto AndroidManifest.xml e o arquivo de configuração de teste "AndroidTest.xml".

Como você está adicionando um teste totalmente novo, provavelmente vai precisar criar o diretório tests ao lado do componente src e preenchê-lo com conteúdo.

Em alguns casos, sua equipe pode ter mais estruturas de diretório em tests devido à necessidade de empacotar diferentes conjuntos de testes em APKs individuais. Nesse caso, você precisará criar um novo subdiretório em tests.

Independente da estrutura, você vai acabar preenchendo o diretório tests ou o subdiretório recém-criado com arquivos semelhantes aos do diretório instrumentation na mudança de gerrit de amostra. Os detalhes de cada arquivo são explicados mais adiante neste documento.

Arquivo de manifesto

Assim como em um projeto de app, cada módulo de teste de instrumentação requer um arquivo de manifesto chamado AndroidManifest.xml. Para incluir automaticamente esse arquivo usando o makefile principal do BUILD_PACKAGE, forneça esse arquivo ao lado do arquivo Android.mk do módulo de teste.

Se você não estiver familiarizado com o arquivo AndroidManifest.xml, consulte a Visão geral do manifesto do app

Confira abaixo um exemplo de arquivo AndroidManifest.xml:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  android:sharedUserId="android.uid.system"
  package="android.test.example.helloworld" >

    <application>
       <uses-library android:name="android.test.runner"/>
    </application>

    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
                     android:targetPackage="android.test.example.helloworld"
                     android:label="Hello World Test"/>

</manifest>

Algumas observações sobre o arquivo de manifesto:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="android.test.example.helloworld" >

O atributo package é o nome do pacote do aplicativo, que é o identificador exclusivo usado pelo framework de aplicativos Android para identificar um aplicativo (ou, neste contexto, seu aplicativo de teste). Cada usuário no sistema só pode instalar um aplicativo com esse nome de pacote.

Além disso, esse atributo package é o mesmo que ComponentName#getPackageName() retorna e também o mesmo que você usaria para interagir com vários subcomandos pm. Use adb shell.

Embora o nome do pacote geralmente tenha o mesmo estilo de um nome de pacote Java, ele tem muito pouco a ver com isso. Em outras palavras, o pacote de aplicativo (ou teste) pode conter classes com qualquer nome de pacote. Por outro lado, você pode optar pela simplicidade e ter o nome do pacote Java de nível superior no aplicativo ou teste idêntico ao nome do pacote do aplicativo.

android:sharedUserId="android.uid.system"

Isso declara que, no momento da instalação, esse arquivo APK deve receber o mesmo ID de usuário, ou seja, a mesma identidade de tempo de execução, da plataforma principal. Isso depende de o APK ser assinado com o mesmo certificado da plataforma principal (consulte LOCAL_CERTIFICATE em uma seção anterior), mas são conceitos diferentes:

  • algumas permissões ou APIs são protegidas por assinatura, o que exige o mesmo certificado de assinatura
  • algumas permissões ou APIs exigem a identidade do usuário system do autor da chamada, o que exige que o pacote de chamada compartilhe o ID do usuário com system, se for um pacote separado da própria plataforma principal
<uses-library android:name="android.test.runner" />

Isso é necessário para todos os testes de instrumentação, já que as classes relacionadas são empacotadas em um arquivo de biblioteca JAR de framework separado. Portanto, são necessárias entradas de classpath adicionais quando o pacote de teste é invocado pelo framework do aplicativo.

android:targetPackage="android.test.example.helloworld"

Talvez você tenha notado que o targetPackage aqui é declarado da mesma forma que o atributo package declarado na tag manifest deste arquivo. Como mencionado em conceitos básicos de teste, essa categoria de teste de instrumentação normalmente é destinada a testar APIs de framework. Portanto, não é muito significativo que elas tenham um pacote de aplicativo segmentado específico, além de si mesmas.

Arquivo de configuração simples

Cada novo módulo de teste precisa ter um arquivo de configuração para direcionar o sistema de build com metadados do módulo, dependências de tempo de compilação e instruções de empacotamento. Na maioria dos casos, a opção de arquivo Blueprint baseada em Soong é suficiente. Para mais detalhes, consulte Configuração simples de teste.

Arquivo de configuração complexo

Para esses casos mais complexos, também é necessário gravar um arquivo de configuração de teste para o framework de teste do Android, o Trade Federation.

A configuração de teste pode especificar opções especiais de configuração do dispositivo e argumentos padrão para fornecer à classe de teste. Consulte o exemplo em /platform_testing/tests/example/instrumentation/AndroidTest.xml.

Confira um snapshot para facilitar:

<configuration description="Runs sample instrumentation test.">
  <target_preparer class="com.android.tradefed.targetprep.TestFilePushSetup"/>
  <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
    <option name="test-file-name" value="HelloWorldTests.apk"/>
  </target_preparer>
  <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer"/>
  <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"/>
  <option name="test-suite-tag" value="apct"/>
  <option name="test-tag" value="SampleInstrumentationTest"/>

  <test class="com.android.tradefed.testtype.AndroidJUnitTest">
    <option name="package" value="android.test.example.helloworld"/>
    <option name="runner" value="android.support.test.runner.AndroidJUnitRunner"/>
  </test>
</configuration>

Algumas observações sobre o arquivo de configuração de teste:

<target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
  <option name="test-file-name" value="HelloWorldTests.apk"/>
</target_preparer>

Isso informa ao Trade Federation para instalar o HelloWorldTests.apk no dispositivo de destino usando um target_preparer especificado. Há muitos preparadores de destino disponíveis para desenvolvedores no Trade Federation, que podem ser usados para garantir que o dispositivo esteja configurado corretamente antes da execução do teste.

<test class="com.android.tradefed.testtype.AndroidJUnitTest">
  <option name="package" value="android.test.example.helloworld"/>
  <option name="runner" value="android.support.test.runner.AndroidJUnitRunner"/>
</test>

Isso especifica a classe de teste do Trade Federation a ser usada para executar o teste e transmite o pacote no dispositivo a ser executado e o framework do executor de testes, que é o JUnit neste caso.

Para mais informações, consulte Test Module Configs.

Recursos do JUnit4

Usar a biblioteca android-support-test como executor de testes permite a adoção de novas classes de teste no estilo JUnit4, e a mudança de exemplo do Gerrit contém alguns usos muito básicos dos recursos dela. Consulte o exemplo em /platform_testing/tests/example/instrumentation/src/android/test/example/helloworld/HelloWorldTest.java.

Embora os padrões de teste sejam geralmente específicos para equipes de componentes, há alguns padrões de uso geralmente úteis.

@RunWith(JUnit4.class)
public class HelloWorldTest {

Uma diferença significativa no JUnit4 é que os testes não precisam mais herdar de uma classe de teste base comum. Em vez disso, você escreve testes em classes Java simples e usa anotações para indicar determinadas configurações e restrições de teste. Neste exemplo, estamos instruindo que essa classe seja executada como um teste do JUnit4.

    @BeforeClass
    public static void beforeClass() {
    ...
    @AfterClass
    public static void afterClass() {
    ...
    @Before
    public void before() {
    ...
    @After
    public void after() {
    ...
    @Test
    @SmallTest
    public void testHelloWorld() {
    ...

As anotações @Before e @After são usadas em métodos pelo JUnit4 para realizar a configuração pré-teste e o encerramento pós-teste. Da mesma forma, as anotações @BeforeClass e @AfterClass são usadas em métodos pelo JUnit4 para realizar a configuração antes de executar todos os testes em uma classe de teste e a desmontagem depois. Os métodos de configuração e desmontagem no escopo da classe precisam ser estáticos. Quanto aos métodos de teste, ao contrário das versões anteriores do JUnit, eles não precisam mais começar o nome do método com test. Em vez disso, cada um deles precisa ser anotado com @Test. Como de costume, os métodos de teste precisam ser públicos, não declarar valor de retorno, não usar parâmetros e podem gerar exceções.

Acesso à classe de instrumentação

Embora não seja abordado no exemplo básico do Hello World, é bastante comum que um teste do Android exija acesso à instância Instrumentation: essa é a interface principal da API que fornece acesso a contextos de aplicativos, APIs de teste relacionadas ao ciclo de vida de atividades e muito mais.

Como os testes do JUnit4 não exigem mais uma classe de base comum, não é mais necessário obter a instância Instrumentation via InstrumentationTestCase#getInstrumentation(). Em vez disso, o novo executor de testes gerencia isso via InstrumentationRegistry, em que a configuração contextual e ambiental criada pelo framework de instrumentação é armazenada.

Para acessar a instância da classe Instrumentation, basta chamar o método estático getInstrumentation() na classe InstrumentationRegistry:

Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation()

Criar e testar localmente

Para os casos de uso mais comuns, use Atest.

Para casos mais complexos que exigem personalização mais pesada, siga as instruções de instrumentação.