Tutorial de teste do Android: teste de unidade como um verdadeiro Droid verde
Publicados: 2022-03-11Como desenvolvedores de aplicativos experientes, à medida que os aplicativos que desenvolvemos amadurecem, temos a sensação de que é hora de começar a testar. As regras de negócios geralmente implicam que o sistema deve fornecer estabilidade em diferentes versões. Idealmente, também queremos automatizar o processo de compilação e publicar o aplicativo automaticamente. Para isso, precisamos de ferramentas de teste Adnroid para garantir que a compilação esteja funcionando conforme o esperado.
Os testes podem fornecer o nível extra de confiança sobre as coisas que construímos. É difícil (se não impossível) construir um produto perfeito e livre de bugs. Portanto, nosso objetivo será melhorar nossas chances de sucesso no mercado, configurando um conjunto de testes que detectará rapidamente os bugs recém-introduzidos em nosso aplicativo.
Quando se trata do Android e das várias plataformas móveis em geral, o teste de aplicativos pode ser um desafio. Implementar testes de unidade e seguir princípios de desenvolvimento orientado a testes ou similar pode muitas vezes parecer pouco intuitivo, no mínimo. No entanto, o teste é importante e não deve ser dado como certo ou ignorado. David, Kent e Martin discutiram os benefícios e as armadilhas dos testes em uma série de conversas entre eles compiladas em um artigo intitulado “O TDD está morto?”. Você também pode encontrar as conversas em vídeo reais e obter mais informações se o teste se encaixa no seu processo de desenvolvimento e até que ponto você pode incorporá-lo, começando agora.
Neste tutorial de teste do Android, vou orientá-lo na unidade e aceitação, teste de regressão no Android. Vamos nos concentrar na abstração da unidade de testes no Android, seguida de exemplos de testes de aceitação, com foco em tornar o processo o mais rápido e simples possível para encurtar os ciclos de feedback do desenvolvedor-QA.
Devo Ler?
Este tutorial irá explorar as diferentes possibilidades quando se trata de testar aplicativos Android. Desenvolvedores ou gerentes de projeto que desejam entender melhor as possibilidades atuais de teste da plataforma Android podem decidir, usando este tutorial, se desejam adotar alguma das abordagens mencionadas neste artigo. No entanto, isso não é uma bala de prata, pois a discussão envolvida em tal tópico varia inerentemente de produto para produto, juntamente com prazos, qualidade do código da base de código, nível de acoplamento do sistema, preferência do desenvolvedor no projeto de arquitetura, vida útil projetada do recurso para teste, etc
Pensando em unidades: teste do Android
Idealmente, queremos testar uma unidade/componente lógica de uma arquitetura de forma independente. Desta forma podemos garantir que nosso componente funcione corretamente para o conjunto de entradas que esperamos. As dependências podem ser simuladas, o que nos permitirá escrever testes que executam rapidamente. Além disso, poderemos simular diferentes estados do sistema com base na entrada fornecida ao teste, abrangendo casos exóticos no processo.
O objetivo do teste de unidade do Android é isolar cada parte do programa e mostrar que as partes individuais estão corretas. Um teste de unidade fornece um contrato estrito e escrito que o código deve satisfazer. Como resultado, oferece vários benefícios. —Wikipédia
Roboelétrica
Robolectric é uma estrutura de teste de unidade Android que permite executar testes dentro da JVM em sua estação de trabalho de desenvolvimento. A Robolectric reescreve as classes do SDK do Android à medida que são carregadas e possibilita que elas sejam executadas em uma JVM normal, resultando em tempos de teste rápidos. Além disso, ele lida com inflação de visualizações, carregamento de recursos e mais coisas implementadas em código C nativo em dispositivos Android, tornando obsoleta a necessidade de emuladores e dispositivos físicos para executar testes automatizados.
Mockito
Mockito é um framework de simulação que nos permite escrever testes limpos em java. Simplifica o processo de criação de duplos de teste (mocks), que são usados para substituir as dependências originais de um componente/módulo usado em produção. Uma resposta do StackOverflow discute as diferenças entre mocks e stubs em termos bastante simples que você pode ler para saber mais.
// you can mock concrete classes, not only interfaces LinkedList mockedList = mock(LinkedList.class); // stubbing appears before the actual execution when(mockedList.get(0)).thenReturn("first"); // the following prints "first" System.out.println(mockedList.get(0)); // the following prints "null" because get(999) was not stubbed System.out.println(mockedList.get(999));
Além disso, com o Mockito podemos verificar se um método foi chamado:
// mock creation List mockedList = mock(List.class); // using mock object - it does not throw any "unexpected interaction" exception mockedList.add("one"); mockedList.clear(); // selective, explicit, highly readable verification verify(mockedList).add("one"); verify(mockedList).clear();
Agora, sabemos que podemos especificar pares de ação-reação que definem o que acontece quando executamos uma ação específica no objeto/componente simulado. Portanto, podemos simular módulos inteiros de nossa aplicação e, para cada caso de teste, fazer com que o módulo simulado reaja de uma maneira diferente. As diferentes maneiras refletirão os possíveis estados do componente testado e do par de componentes simulados.
Teste de unidade
Nesta seção, assumiremos a arquitetura MVP (Model View Presenter). As atividades e fragmentos são as visualizações, os modelos sendo a camada de repositório para chamadas ao banco de dados ou serviços remotos, e o apresentador sendo o “cérebro” que une tudo isso implementando lógica específica para controlar visualizações, modelos e o fluxo de dados através do inscrição.
Abstraindo Componentes
Vistas e modelos de simulação
Neste exemplo de teste do Android, simularemos visualizações, modelos e componentes de repositório e testaremos a unidade do apresentador. Este é um dos menores testes, visando um único componente na arquitetura. Além disso, usaremos o método stub para configurar uma cadeia de reações adequada e testável:
@RunWith(RobolectricTestRunner.class) @Config(manifest = "app/src/main/AndroidManifest.xml", emulateSdk = 18) public class FitnessListPresenterTest { private Calendar cal = Calendar.getInstance(); @Mock private IFitnessListModel model; @Mock private IFitnessListView view; private IFitnessListPresenter presenter; @Before public void setup() { MockitoAnnotations.initMocks(this); final FitnessEntry entryMock = mock(FitnessEntry.class); presenter = new FitnessListPresenter(view, model); /* Define the desired behaviour. Queuing the action in "doAnswer" for "when" is executed. Clear and synchronous way of setting reactions for actions (stubbing). */ doAnswer((new Answer<Object>() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { ArrayList<FitnessEntry> items = new ArrayList<>(); items.add(entryMock); ((IFitnessListPresenterCallback) presenter).onFetchAllSuccess(items); return null; } })).when(model).fetchAllItems((IFitnessListPresenterCallback) presenter); } /** Verify if model.fetchItems was called once. Verify if view.onFetchSuccess is called once with the specified list of type FitnessEntry The concrete implementation of ((IFitnessListPresenterCallback) presenter).onFetchAllSuccess(items); calls the view.onFetchSuccess(...) method. This is why we verify that view.onFetchSuccess is called once. */ @Test public void testFetchAll() { presenter.fetchAllItems(false); // verify can be called only on mock objects verify(model, times(1)).fetchAllItems((IFitnessListPresenterCallback) presenter); verify(view, times(1)).onFetchSuccess(new ArrayList<>(anyListOf(FitnessEntry.class))); } }
Zombando da camada de rede global com MockWebServer
Muitas vezes, é conveniente poder zombar da camada de rede global. O MockWebServer nos permite enfileirar respostas para solicitações específicas que executamos em nossos testes. Isso nos dá a chance de simular respostas obscuras que esperamos do servidor, mas não são fáceis de reproduzir. Isso nos permite garantir cobertura total enquanto escrevemos pouco código adicional.
O repositório de código do MockWebServer fornece um exemplo interessante que você pode consultar para entender melhor essa biblioteca.
Duplas de teste personalizadas
Você pode escrever seu próprio modelo ou componente de repositório e injetá-lo no teste fornecendo um módulo diferente para o gráfico do objeto usando o Dagger (http://square.github.io/dagger/). Temos a opção de verificar se o estado da visualização foi atualizado corretamente com base nos dados fornecidos pelo componente do modelo simulado:
/** Custom mock model class */ public class FitnessListErrorTestModel extends FitnessListModel { // ... @Override public void fetchAllItems(IFitnessListPresenterCallback callback) { callback.onError(); } @Override public void fetchItemsInRange(final IFitnessListPresenterCallback callback, DateFilter filter) { callback.onError(); } }
@RunWith(RobolectricTestRunner.class) @Config(manifest = "app/src/main/AndroidManifest.xml", emulateSdk = 18) public class FitnessListPresenterDaggerTest { private FitnessActivity activity; private FitnessListFragment fitnessListFragment; @Before public void setup() { /* setupActivity runs the Activity lifecycle methods on the specified class */ activity = Robolectric.setupActivity(FitnessActivity.class); fitnessListFragment = activity.getFitnessListFragment(); /* Create the objectGraph with the TestModule */ ObjectGraph localGraph = ObjectGraph.create(TestModule.newInstance(fitnessListFragment)); /* Injection */ localGraph.inject(fitnessListFragment); localGraph.inject(fitnessListFragment.getPresenter()); } @Test public void testInteractorError() { fitnessListFragment.getPresenter().fetchAllItems(false); /* suppose that our view shows a Toast message with the specified text below when an error is reported, so we check for it. */ assertEquals(ShadowToast.getTextOfLatestToast(), "Something went wrong!"); } @Module( injects = { FitnessListFragment.class, FitnessListPresenter.class }, overrides = true, library = true ) static class TestModule { private IFitnessListView view; private TestModule(IFitnessListView view){ this.view = view; } public static TestModule newInstance(IFitnessListView view){ return new TestModule(view); } @Provides public IFitnessListInteractor provideFitnessListInteractor(){ return new FitnessListErrorTestModel(); } @Provides public IFitnessListPresenter provideFitnessPresenter(){ return new FitnessListPresenter(view); } } }
Executando testes
Android Studio
Você pode facilmente clicar com o botão direito em uma classe de teste, método ou pacote de teste inteiro e executar os testes na caixa de diálogo de opções no IDE.
terminal
A execução de testes de aplicativos Android a partir do terminal cria relatórios para as classes testadas na pasta “build” do módulo de destino. Ainda mais, se você planeja configurar um processo de compilação automatizado, usará a abordagem de terminal. Com o Gradle, você pode executar todos os testes com sabor de depuração executando o seguinte:
gradle testDebug
Acessando o Source Set “test” da versão do Android Studio
A versão 1.1 do Android Studio e o plug-in Android Gradle oferecem suporte para teste de unidade do seu código. Você pode aprender mais lendo sua excelente documentação sobre ele. O recurso é experimental, mas também uma ótima inclusão, pois agora você pode alternar facilmente entre seus testes de unidade e conjuntos de fontes de teste de instrumentação do IDE. Ele se comporta da mesma maneira como se você trocasse de sabores no IDE.
Facilitando o processo
Escrever testes de aplicativos Android pode não ser tão divertido quanto desenvolver o aplicativo original. Por isso, algumas dicas de como facilitar o processo de redação de testes e evitar problemas comuns durante a configuração do projeto ajudarão bastante.
Assert J Android
AssertJ Android, como você deve ter adivinhado pelo nome, é um conjunto de funções auxiliares criadas com o Android em mente. É uma extensão da popular biblioteca AssertJ. A funcionalidade fornecida pelo AssertJ Android varia de afirmações simples, como “assertThat(view).isGone()”, a coisas tão complexas como:

assertThat(layout).isVisible() .isVertical() .hasChildCount(4) .hasShowDividers(SHOW_DIVIDERS_MIDDLE)
Com o AssertJ Android e sua extensibilidade, você garante um bom e simples ponto de partida para escrever testes para aplicativos Android.
Caminho Robolétrico e Manifesto
Ao usar o Robolectric, você pode notar que precisa especificar o local do manifesto e que a versão do SDK está definida como 18. Você pode fazer isso incluindo uma anotação “Config”.
@Config(manifest = "app/src/main/AndroidManifest.xml", emulateSdk = 18)
A execução de testes que exigem o Robolectric do terminal pode apresentar novos desafios. Por exemplo, você pode ver exceções como “Tema não definido”. Se os testes estiverem sendo executados corretamente no IDE, mas não no terminal, você pode estar tentando executá-lo a partir de um caminho no terminal em que o caminho do manifesto especificado não pode ser resolvido. O valor de configuração codificado para o caminho do manifesto pode não estar apontando para o local correto do ponto de execução do comando. Isso pode ser resolvido com o uso de runners personalizados:
public class RobolectricGradleTestRunner extends RobolectricTestRunner { public RobolectricGradleTestRunner(Class<?> testClass) throws InitializationError { super(testClass); } @Override protected AndroidManifest getAppManifest(Config config) { String appRoot = "../app/src/main/"; String manifestPath = appRoot + "AndroidManifest.xml"; String resDir = appRoot + "res"; String assetsDir = appRoot + "assets"; AndroidManifest manifest = createAppManifest(Fs.fileFromPath(manifestPath), Fs.fileFromPath(resDir), Fs.fileFromPath(assetsDir)); return manifest; } }
Configuração do Gradle
Você pode usar o seguinte para configurar o Gradle para teste de unidade. Pode ser necessário modificar os nomes de dependência e as versões necessárias com base nas necessidades do seu projeto.
// Robolectric testCompile 'junit:junit:4.12' testCompile 'org.mockito:mockito-core:1.9.5' testCompile 'com.squareup.dagger:dagger:1.2.2' testProvided 'com.squareup.dagger:dagger-compiler:1.2.2' testCompile 'com.android.support:support-v4:21.0.+' testCompile 'com.android.support:appcompat-v7:21.0.3' testCompile('org.robolectric:robolectric:2.4') { exclude module: 'classworlds' exclude module: 'commons-logging' exclude module: 'httpclient' exclude module: 'maven-artifact' exclude module: 'maven-artifact-manager' exclude module: 'maven-error-diagnostics' exclude module: 'maven-model' exclude module: 'maven-project' exclude module: 'maven-settings' exclude module: 'plexus-container-default' exclude module: 'plexus-interpolation' exclude module: 'plexus-utils' exclude module: 'wagon-file' exclude module: 'wagon-http-lightweight' exclude module: 'wagon-provider-api' }
Serviços Robolétricos e de Brincar
Se você estiver usando o Google Play Services, terá que criar sua própria constante inteira para a versão do Play Services para que o Robolectric funcione corretamente nesta configuração do aplicativo.
<meta-data android:name="com.google.android.gms.version" android:value="@integer/gms_version" tools:replace="android:value" />
Dependências Roboelétricas para Suporte a Bibliotecas
Outro problema de teste interessante é que o Robolectric não é capaz de referenciar as bibliotecas de suporte corretamente. A solução é adicionar um arquivo “project.properties” ao módulo onde estão os testes. Por exemplo, para as bibliotecas Support-v4 e AppCompat, o arquivo deve conter:
android.library.reference.1=../../build/intermediates/exploded-aar/com.android.support/support-v4/21.0.3 android.library.reference.2=../../build/intermediates/exploded-aar/com.android.support/appcompat-v7/21.0.3
Teste de aceitação/regressão
O teste de aceitação/regressão automatiza parte da etapa final do teste em um ambiente real 100% Android. Não usamos classes de SO Android simuladas neste nível - os testes são executados em dispositivos e emuladores reais.
Essas circunstâncias tornam o processo muito mais instável devido à variedade de dispositivos físicos, configurações de emulador, estados de dispositivos e conjuntos de recursos de cada dispositivo. Além disso, depende muito da versão do sistema operacional e do tamanho da tela do telefone para decidir como o conteúdo será exibido.
É um pouco complexo criar o teste certo que passe em uma ampla variedade de dispositivos, mas, como sempre, você deve sonhar grande e começar pequeno. A criação de testes com Robotium é um processo iterativo. Com alguns truques, pode ser muito simplificado.
Robotium
Robotium é uma estrutura de automação de testes Android de código aberto que existe desde janeiro de 2010. Vale ressaltar que Robotium é uma solução paga, mas vem com um teste gratuito justo.
Para acelerar o processo de escrita de testes Robotium, vamos nos afastar da escrita manual de testes para a gravação de testes. O trade-off é entre qualidade de código e velocidade. Se você estiver fazendo mudanças pesadas em sua interface de usuário, você se beneficiará muito da abordagem de gravação de teste e poderá gravar novos testes rapidamente.
Testdroid Recorder é um gravador de teste gratuito que cria testes do Robotium à medida que registra os cliques que você executa na interface do usuário. A instalação da ferramenta é super fácil, conforme descrito em suas documentações acompanhadas de um vídeo passo a passo.
Como o Testdroid Recorder é um plug-in do Eclipse e estamos nos referindo ao Android Studio ao longo deste artigo, idealmente seria um motivo de preocupação. No entanto, neste caso não é um problema, pois você pode usar o plugin diretamente com um APK e gravar os testes nele.
Depois de criar os testes, você pode copiá-los e colá-los no Android Studio, junto com qualquer dependência exigida pelo gravador Testdroid, e pronto. O teste gravado seria algo como a classe abaixo:
public class LoginTest extends ActivityInstrumentationTestCase2<Activity> { private static final String LAUNCHER_ACTIVITY_CLASSNAME = "com.toptal.fitnesstracker.view.activity.SplashActivity"; private static Class<?> launchActivityClass; static { try { launchActivityClass = Class.forName(LAUNCHER_ACTIVITY_CLASSNAME); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } } private ExtSolo solo; @SuppressWarnings("unchecked") public LoginTest() { super((Class<Activity>) launchActivityClass); } // executed before every test method @Override public void setUp() throws Exception { super.setUp(); solo = new ExtSolo(getInstrumentation(), getActivity(), this.getClass() .getCanonicalName(), getName()); } // executed after every test method @Override public void tearDown() throws Exception { solo.finishOpenedActivities(); solo.tearDown(); super.tearDown(); } public void testRecorded() throws Exception { try { assertTrue( "Wait for edit text (id: com.toptal.fitnesstracker.R.id.login_username_input) failed.", solo.waitForEditTextById( "com.toptal.fitnesstracker.R.id.login_username_input", 20000)); solo.enterText( (EditText) solo .findViewById("com.toptal.fitnesstracker.R.id.login_username_input"), "[email protected]"); solo.sendKey(ExtSolo.ENTER); solo.sleep(500); assertTrue( "Wait for edit text (id: com.toptal.fitnesstracker.R.id.login_password_input) failed.", solo.waitForEditTextById( "com.toptal.fitnesstracker.R.id.login_password_input", 20000)); solo.enterText( (EditText) solo .findViewById("com.toptal.fitnesstracker.R.id.login_password_input"), "123456"); solo.sendKey(ExtSolo.ENTER); solo.sleep(500); assertTrue( "Wait for button (id: com.toptal.fitnesstracker.R.id.parse_login_button) failed.", solo.waitForButtonById( "com.toptal.fitnesstracker.R.id.parse_login_button", 20000)); solo.clickOnButton((Button) solo .findViewById("com.toptal.fitnesstracker.R.id.parse_login_button")); assertTrue("Wait for text fitness list activity.", solo.waitForActivity(FitnessActivity.class)); assertTrue("Wait for text KM.", solo.waitForText("KM", 20000)); /* Custom class that enables proper clicking of ActionBar action items */ TestUtils.customClickOnView(solo, R.id.action_logout); solo.waitForDialogToOpen(); solo.waitForText("OK"); solo.clickOnText("OK"); assertTrue("waiting for ParseLoginActivity after logout", solo.waitForActivity(ParseLoginActivity.class)); assertTrue( "Wait for button (id: com.toptal.fitnesstracker.R.id.parse_login_button) failed.", solo.waitForButtonById( "com.toptal.fitnesstracker.R.id.parse_login_button", 20000)); } catch (AssertionFailedError e) { solo.fail( "com.example.android.apis.test.Test.testRecorded_scr_fail", e); throw e; } catch (Exception e) { solo.fail( "com.example.android.apis.test.Test.testRecorded_scr_fail", e); throw e; } } }
Se você olhar de perto, notará o quanto do código é bastante direto.
Ao gravar os testes, não se esgote em declarações de “espera”. Aguarde até que os diálogos apareçam, as atividades apareçam, os textos apareçam. Isso garantirá que a atividade e a hierarquia de visualização estejam prontas para interagir quando você executar a ação na tela atual. Ao mesmo tempo, faça capturas de tela. Os testes automatizados geralmente são autônomos e as capturas de tela são uma das maneiras de ver o que realmente aconteceu durante esses testes.
Quer os testes sejam aprovados ou reprovados, os relatórios são seus melhores amigos. Você pode encontrá-los no diretório de compilação “module/build/outputs/reports”:
Em teoria, a equipe de controle de qualidade poderia registrar testes e otimizá-los. Ao colocar esforço em um modelo padronizado para otimizar os casos de teste, isso pode ser feito. Quando você normalmente grava testes, sempre precisa ajustar algumas coisas para que funcione perfeitamente.
Por fim, para executar esses testes no Android Studio, você pode selecioná-los e executá-los como faria com testes de unidade. A partir do terminal, é um one-liner:
gradle connectedAndroidTest
Desempenho dos Testes
O teste de unidade do Android com o Robolectric é extremamente rápido, pois é executado diretamente na JVM em sua máquina. Comparado a isso, o teste de aceitação em emuladores e dispositivos físicos é muito mais lento. Dependendo do tamanho dos fluxos que você está testando, pode levar de alguns segundos a alguns minutos por caso de teste. A fase de teste de aceitação deve ser usada como parte de um processo de construção automatizado em um servidor de integração contínua.
A velocidade pode ser melhorada pela paralelização em vários dispositivos. Confira esta ótima ferramenta de Jake Wharton e os caras da Square http://square.github.io/spoon/. Tem algumas reportagens legais também.
O take-away
Há uma variedade de ferramentas de teste do Android disponíveis e, à medida que o ecossistema amadurece, o processo de configurar um ambiente testável e escrever testes se tornará mais fácil. Ainda há mais desafios a serem enfrentados e, com uma ampla comunidade de desenvolvedores trabalhando em problemas diários, há muito espaço para discussões construtivas e feedback rápido.
Use as abordagens descritas neste tutorial de teste do Android para orientá-lo a enfrentar os desafios à sua frente. Se e quando você tiver problemas, consulte este artigo ou as referências vinculadas para obter soluções para problemas conhecidos.
Em um post futuro, discutiremos paralelização, automação de compilação, integração contínua, ganchos Github/BitBucket, controle de versão de artefato e as melhores práticas para gerenciar projetos de aplicativos móveis massivos com mais profundidade.