Trabalhando com Unit Tests em Android

Olá pessoal.

Meu nome é Athila Santos. Eu trabalho com o Felipe em desenvolvimento Android na Motorola e vou começar a ajudá-lo na publicação de artigos sobre Android para este blog.

O meu primeiro artigo será sobre Unit Tests.

Para quem não sabe, Unit Tests são testes realizados pelo proprio desenvolvedor ou analista de teste, visando testar a menor parte testável de um sistema.

No entanto, testes realizados envolvendo contato com o dispositivo podem ser realizados por qualquer pessoa que utilize a aplicação. O que nós, developers, queremos é testar um determinado trecho de código, estado de classes etc. Para tanto, vamos desenvolver APLICAÇÕES responsáveis, unicamente, por testar a nossa aplicação.

É bom frisar que os Unit Tests são parte essencial do ciclo de desenvolvimento de um projeto. Para uma app pequena como a QuickNotes pode parecer perda de tempo, mas grandes projetos não sobrevivem sem que os desenvolvedores gastem um bom tempo desenvolvendo Unit Tests decentes.

A plataforma Java nos fornece uma excelente ferramenta para desenvolvimento de Unit Tests. É a JUnit. Para ficar ainda melhor, Android também nos fornece todo um framework de
classes em complemento às já existentes no framework do JUnit.

Dada esta introdução, mãos à obra!

Neste artigo, vamos testar apenas a UI da Activity principal do QuickNotes.

Passo 1 – Criar o projeto de teste para o QuickNotes.

No Eclipse, clique com o botão direito no seu projeto QuickNotes -> New -> Other
Expanda a pasta “Android” e selecione “Android Test Project”.
Preencha os campos como na figura 1 (ao selecionar o projeto QuickNotes no campo “An existing Android Project”, os campos seguintes serão preenchidos automaticamente.

Criando o projeto

Clique em “Finish” e seu projeto irá aparecer no Package Explorer.

Passo 2 – Criar sua classe de test case.

Com o botão direito, clique no projeto QuickNotesTest -> New -> Class
Preencha os campos conforme a figura 2. (a classe android.test.ActivityInstrumentationTestCase2<T> é uma classe presente no framework de testes do Android que nos provê algumas APIs muito úteis para interagir com a UI de uma Activity). Ela é parametrizada com o nome da classe que será testada por esta classe.

Criando a classe de Teste

Com a classe de teste criada, vamos implementá-la.
A primeira coisa que uma classe de teste precisa é de um construtor. No construtor padrão de qualquer classe de teste que tenha como pai a classe ActivityInstrumentationTestCase2<T>, nós precisamos chamar o construtor desta passando como parametro o pacote da classe sendo testada e uma instancia representando tal classe (.class). Para o nosso caso, o construtor terá apenas isso. Portanto:

public MainActivityTest() {
    super("com.exemplos.quicknotes", MainActivity.class);
}

O método setUp() é um método herdado de uma das classes pai e é destinado à inicialização do estado da classe de teste. Este método é executado ANTES DE CADA TEST CASE, sempre. No nosso caso, vamos usá-lo para pegar os elementos de UI da  nossa MainActivity:

@Override
protected void setUp() throws Exception {
    super.setUp();
    mActivity = getActivity(); // mActivity ira guardar uma instânca da MainActivity.
    // Esta chamada so e possivel por causa dos parametros passados no construtor.
    // e o parametro passado na definição da classe
    mEditor = (EditText) mActivity.findViewById(R.id.edit_box);
    mButton = (Button) mActivity.findViewById(R.id.insert_button);
    mList = (ListView) mActivity.findViewById(android.R.id.list);
    mAdapter = (SimpleCursorAdapter) mList.getAdapter();
}

Obviamente, você deve criar as variáveis de instância envolvidas no método:

private MainActivity mActivity;
private EditText mEditor;
private Button mButton;
private ListView mList;
private SimpleCursorAdapter mAdapter;

Vamos também criar a seguinte constante na classe (será explicado posteriormente)

// Altere este valor para o numero de insercoes de teste vc deseja executar
private static int NUMERO_DE_INSERCOES = 10;

Antes de começar a implementar os test cases, é importante verificar se o estado da classe esta consistente. Para executar checagens, vamos usar APIs do framework do JUnit. Métodos como assertTrue, assertEquals, etc são responsáveis por passar ou falhar um determinado check.

// Testa se os elementos da tela foram inicializados corretamente
private void testPreConditions() {
    assertTrue(mActivity != null);
    assertTrue(mEditor != null);
    assertTrue(mButton != null);
    assertTrue(mList != null);
    assertTrue(mAdapter != null);
    assertTrue(NUMERO_DE_INSERCOES &gt; 0);
}

Agora, podemos começar a escrever nossos test cases. Cada test case é um método publico, sem retorno (void) e sem parametros. Também devem ter seu nome começado pela palavra “test”. Assim, o Android Test Runner (entidade responsável por excutar os testes) saberá quais metodos ele deve executar como testes. Portanto, os test cases devem ter o seguinte padrão:

public void test<NomeDoMetodo>()

No nosso caso, vamos executar testes de inserção de elementos na lista e verificar se o que foi digitado pelo usuário é o que realmente aparece na tela.
Para melhorar a exeperiência, altere o arquivo de xml que descreve o layout da MainActivity do QuickNotes (QuickNotes\res\layout\main.xml) e adicione o seguinte atributo ao ListView:

android:transcriptMode=”alwaysScroll”

ou seja, deixa o elemento desta forma:

<ListView android:id="@id/android:list"
                 android:layout_width="fill_parent"
                 android:layout_height="wrap_content"
                 android:transcriptMode="alwaysScroll">
</ListView>

Isto fará com que a lista role automaticamente após uma inserção, se os itens não couberem mais na tela.

Portanto, nosso test case fica assim:

public void testInsertIntoTheList() {
    // Como nao podemos assegurar a ordem em que os testes sao executados, devemos
    // verificar pre-condicoes antes de cada teste.
    testPreConditions();

    for (int i = 0; i &lt; NUMERO_DE_INSERCOES; i++) {
        // Codigos de teste que interagem com elementos da UI (Views) devem rodar na thread
        // principal, tambem chamada de UI thread.
        mActivity.runOnUiThread(new Runnable() {
           @Override
           public void run() {
               // Vamos pedir o foco para o editor e comecar a digitar
               mEditor.requestFocus();
           }
        });

       // Vamos "digitar" no edit box uma entrada com o conteudo
       // "random[numero aleatorio entre 0 e 100]", navegar para a direita
       // (DPAD_RIGHT) - onde esta o botao de Insert e clicar na center key
       // para efetuar a insercao
       Integer rand = (int)(Math.random()*100);
       char[] digits = rand.toString().toCharArray(); 
       if (digits.length == 1) {
            this.sendKeys("R A N D O M "+digits[0]+" DPAD_RIGHT DPAD_CENTER");
       } else {
            this.sendKeys("R A N D O M "+digits[0]+" "+digits[1]+" DPAD_RIGHT DPAD_CENTER");
       }

       // Apos digitar uma entrada, comparamos se o que foi digitado pelo usuario e
       // o que esta sendo de fato mostrado na tela. Lembre-se que o objetivo aqui e
       // testar a UI e nao o DB. Android oferece todo um set de classes special para
       // executar test cases para operacoes envolvendo o DB. Por esta razao, neste momento,
       // nao nos interessa o que esta guardado no cursor que foi passado para o
       // adapter e sim o que esta sendo mostrado na tela (os dois devem bater, obviamente)
       LinearLayout item = (LinearLayout)(mList.getChildAt(mList.getChildCount() - 1));
       TextView view = (TextView) (item.findViewById(R.id.text));
       String text = view.getText().toString();
       assertEquals("random"+rand, text);
    }
}

Repare que executamos uma chamada à funcao testPreConditions no inicio. É muito importante frisar que a ordem em que os testes cases são executados NÃO É ASSEGURADA! Portanto, se nós mudássemos o modificador de acesso dessa função para “public” ela atenderia todos os requisitos para ser um test case (sem retorno, sem parametros e comecado pela palavra “test”) e seria executado pelo Android Test Runner.

No entanto, ninguém garante que este método seria executado antes do nosso teste. Portanto, a regra é: todos os test cases que escrevemos devem ser independentes entre si e não possuir nenhuma relação uns com os outros. Caso você queira garantir uma ordem de execução, deve fazer como fizemos neste exemplo – tornar a função private e chamá-la no inicio de cada test case.

Veja também que usamos a API sendKeys da nossa classe-pai ActivityInstrumentationTestCase2<T>. Esta API envia eventos de teclas para o dispositivo como se fosse o proprio usuário que o estivesse fazendo.

Neste post vamos desenvolver apenas este teste. Á medida que novas funcionalidades forem adicionadas à aplicação, vamos desenvolver novos testes para a UI e publicar em artigos posteriores.

Passo 3 – Executando os testes

Executar os testes é muito simples: clique com o botão direito no projeto QuickNotesTest -> Run As -> Android JUnit Test

Se suas configurações de AVD estiverem corretas, o simulador será iniciado e o QuickNotes será executado. Você verá várias entradas sendo “digitadas” no text field e adicionadas à lista uma a uma.

No Eclipse vc pode acompanhar o andamento e o resultado dos testes, como na figura abaixo.

Executando os Testes

Para efeito de demonstração, vamos forçar uma falha. Altere a seguinte linha do testcase:

– assertEquals(“random”+rand, text);
+ assertEquals(“test_falha”+rand, text);

Agora, clique no botão “Rerun Test” (icone verde com uma seta amarela). Você verá o resultado como na figura abaixo. Repare que o Eclipse te fornece a stack trace de onde ocorreu a falha. Assim, você é capaz de saber exatamente onde ocorreu a falha e corrigir o problema.

Executando o JUnit

Isto termina nossa introdução aos Unit Tests em Android. No próximo post, vamos aprender como testar a consistência de estado de uma Activity (se o usuário recebe uma ligação, por exemplo, enquanto está usando sua aplicação, como saber se sua Activity vai retornar ao estado anterior depois que a ligação se encerrar?).

Até lá!

Desenvolvendo para Android

22 respostas to “Trabalhando com Unit Tests em Android”

  1. Excelente artigo Athila!

  2. Itim disse:

    A cada dia aprendo mais aki!!!!

  3. Bianco disse:

    Ótimo Athila, parabéns! Segui o exemplo e aprendi a fazer teste unitário,como ficaria para testar o retorno de uma mensagem na tela,
    ex: Tela de login, digito um usuário inválido e uma msg é exibida.

  4. Bianco disse:

    Desculpe, o post anterior era uma pergunta.

  5. Athila Santos disse:

    Olá Bianco!
    Não sei se entendi bem a sua pergunta. Você quer testar se a sua app está executando com sucesso uma pré-checagem do username baseado em uma expressão regular antes de mandar pro server ou quer testar se o server está enviando corretamente uma mensagem de erro?
    No primeiro caso, é fácil testar: Java oferece duas classes para manipular regex’s: Pattern e Matcher. A primeira é utilizada para compilar a regex e a segunda para fazer o match utilizando o objeto pattern que você criou e o texto de entrada (você pode saber mais sobre estas classes consultando a documanetação Java em http://download.oracle.com/javase/6/docs/api/). Aí é só criar um Pattern utilizando a mesma regex que você utilizou na sua app para validar (localmente) os dados digitados pelo usuário (checar se o username contém caracteres como #,@,&,*, por exemplo) e usar os métodos de asserções falados neste artigo para passar ou falhar o teste).

    No segundo caso já é mais complicado, pois varia muito com a arquitetura de software que você está utilizando. Depende de como você está tratando respostas vindas do server. Se você usa listeners, observers etc, você pode criar uma classe de teste execute seu comando de login (passando os dados inválidos) e, em suas callbacks que tratam erro, utilizar métodos de asserções para testar a resposta do server. Mas neste caso, nós não estamos testando UI. Portando, você não deve herdar sua classe de teste da ActivityInstrumentationTestCase2.
    Lembre-se que você não precisa ficar preso ao framework de UT provido pelo Android ou até mesmo ao framework do JUnit. Você pode criar suas próprias classes de teste livremente e utilizar asserções do próprio framework Java (classe Assertion)

    Espero ter ajudado.

  6. Renato disse:

    Parabéns excelente material…

  7. Carlos Henrique de Oliveira disse:

    Athila, ve se tem como me ajudar.

    TENHO ARQUIVOS EM PDF E WORD E PRECISO ABRI-LOS NO ANDROID.
    EX. TENHO UMA LIVRO NO QUAL FOI DIGITADO NO WORD ESTA EM MEU COMPUTADOR E QUERO COLOCA-LO NO MEU CELULAR OU TABLET ANDROID.

    AGUARDO SUA RESPOSTA

  8. CARLOS HENRIQUE FERREIRA VIANA disse:

    BOM MATERIAL ATHILA, ESTOU INICIANDO NO ANDROID AGORA POIS ESTOU COM UM PROBLEMAO NA MAO PRA RESOLVER, MEU CELULAR ESTA DANDO LOOP NA INICIALIZACAO E FICA REINICIANDO E NAO PARA ESTOU BRIGANDO PRA SUBIR OUTRA VERSAO DO ANDROID NO APARELHO E NAO CONSIGO.

    SE VOCE PUDER ME DAR UMAS DICAS DE COMO FAZER AGRADEÇO.

    CARLOS HENRIQUE F. VIANA

  9. Adriano disse:

    Olá, acompanhei todo o material, muito bom, mas nessa parte de teste não consegui executa-lo.

    Mensagem = QuickNotesTeste] Test run failed: Test run failed to complete. Expected 2 tests, received 1

    Estou quebrando a cabeça, mais sou iniciante em programação para android, qualquer ajuda agradeço.

  10. Athila Santos disse:

    Olá Adriano. Preciso de mais informação para lhe ajudar.
    Copie e cole toda a stacktrace do erro para que eu de uma olhada.

  11. Duarte disse:

    Eu também estive a ver o teste e não estava a conseguir executar, falta uma linha que tem que se colocada antes do ciclo for dentro da função de teste:

    PreConditions();
    this.sendKeys(“DPAD_RIGHT DPAD_CENTER”);
    for (int i = 0; i < NUMERO_DE_INSERCOES; i++) {

    O que lhe deve estar a acontecer é que o teste não passa a primeira actividade que mostra o aviso.

  12. Boa Tarde Athila,

    Estava seguindo seu exemplo e na hora de utilizar o R.id.insert_button (entre outros) o eclipse me acusou um erro dizendo que não podeia resolver os atributos. Ao abrir a classe R.java do projeto de testes verifiquei que ela não esta igual a classe R.java do meu projeto QuickNotes, tem algo que eu preciso fazer em especifico para funcionar?

    Desde já agradeço pela atenção.

  13. Athila Santos disse:

    Boa tarde Marcos.
    Tente fazer um clean nos dois projetos e execute os testes novamente (Project -> Clean). Este passo resolve grande parte dos problemas envolvendo a classe R.

  14. GeovaneSilveira disse:

    Opa!
    blz? Cara sou iniciante em programming for android e estou precisando de uma
    ajuda em um exercicio bem simples ,mais esta me dando uma certa dor de cabeça… preciso setar um texto com botão tudo em java … até aeh blz . mais tenho que fazer voltar o texto antigo, algo como interruptor!

    Então criei o button e na TextView renomei e dei o set… tipo:
    botao com nome interruptor quando acionado TextView mostra Ligado, dai
    preciso apertar o botao e fazer voltar texto como Desligado.”Exatamente como se fosse um interruptor de lâmpada.”

    Blz. o Desafio é fazer esta aplicação em android , porem implementar isto em codigo java somente…

    Cara , se possível ajudar fico grato.

  15. NELSON disse:

    Bom dia,

    Talves um dos dois possa me orientar, já que trabalham na motorola. Tenho um tablet xoom 2 me ics 4.0.4. O audio só está saindo no fone de ouvido. Os alto falantes não estão funcionando. Tem alguma configuração onde se possa corrigir esse problema? No aguardo, Nelson.

  16. Márcio Silva disse:

    Olá! Sou técnico em informática(hardwares e softwares), também programador em VB e VBNet. E estou a me interessar em fazer apps para Android. Começando do zero mesmo! Procurando um site sobre o assunto, achei este e, realmente, excelente! Parabéns. Já baixei os programas que precisa-se!
    Abraço.

  17. Davidson Campos disse:

    Estou acompanhando o curso desde o início e estou impressionado com a qualidade do material, abordando tecnicas complexas com muita simplicidade usando exemplos muito práticos do dia a dia de um desenvolvedor e não aquele falatorio teorico, parabéns aos dois professores.

    Só não entendo porque pessoas com tablets quebrados ou que tem exercícios de faculdade a resolver vem pedir ajuda nos posts do curso, a imbecilidade do ser humano me impressiona sempre que pode…

  18. Obrigado Davidson!
    Nesta semana teremos novidades; estou revisando todo o material e terminando um novo capítulo.
    Obrigado por acompanhar o curso!

  19. Tiago disse:

    Eaw cara, paro com o projeto? Tava muito bom.

  20. Oi Tiago, não parei não 🙂
    Neste último final de semana já coloquei o capitulo mais recente, sobre HTTP. E essa semana tem mais!

  21. Alex Tayron disse:

    Boa tarde, Felipe…
    estou em um projeto de criar um app
    me parecia bem simples no começo, mas estou meio atrapalhado agora..
    teria como vc fazer um projeto e nos ensinar a desenvolver algo parecido com o whatsapp??
    nao precisa fazer ligações, apenas envio de msgs em tempo real, adicionando numeros
    e etc…

Deixe um comentário