Testes de Interface com o Espresso

Não, não é expresso, nem café, nem trem.

Espresso é uma API utilizada pela Google para testes de interface de aplicativos Android que foi aberta ao público no final de 2013, juntamente com o Google Instrumentation Test Runner (GITR), que juntos oferecem a possibilidade de escrever testes mais legíveis e confiáveis aos desenvolvedores.

Aí você deve se perguntar, por que eu deveria usar isso?

Afinal, existem várias outras APIs que oferecem funções para testes de interface, inclusive talvez já use alguma delas. A principal vantagem, e só ela já praticamente bastaria, é o sincronismo. Todas as chamadas dos testes rodam de forma síncrona com as chamadas da interface de usuário. E o que isso significa? Bom, basicamente, nada mais de sleeps, waitForSomething, timeout, nada. Nada também daquele teste que “na minha máquina rodava”. Nada de quebrar o build por causa de um timeout que não foi o suficiente.

Além disso, com o Test Runner, é possível deixar de se preocupar tanto com o ciclo das Activities, evitando aqueles bugs aleatórios que surgem quando aquela Activity não teve tempo de fechar, começou o outro teste e apareceu aquela Exception que não fazia o menor sentido, e aí, até achar o bug…

E como isso acontece? Basicamente, o GITR garante que o método onCreate seja finalizado antes de começar qualquer coisa, além de certificar-se de que todas as Activities iniciadas sejam finalizadas com o teste, sem que você precise escrever uma linha de código para isso.

Tudo bem, muito bonita essa parte técnica, mas e sobre o código dos testes?

Os dois métodos base para realizar os testes com o Espresso são o onView e onData (já explico a diferença), que retornam, respectivamente, uma ViewInteraction e uma DataInteraction, ou seja, você nunca vai lidar com a View (Button, TextView, EditText …) em si. Por exemplo, para buscar uma View no layout com o texto “Bem-vindo”, podemos utilizar a seguinte chamada:

onView(withText(“Bem-vindo”));

(Sim, isso mesmo que você leu)

Geralmente, utilizando os métodos withId ou withText é possível encontrar a maioria das Views, mas além destes, existem uma série de métodos dentro da classe ViewMatchers (você pode baixar o código fonte ou olhar aqui) que podem ser utilizados. Além disso, utilizando Harmcrest Matchers (o nome pode ser estranho, mas se você usa o Mockito provavelmente já usou) como allOf, anyOf, not (lógicos), containsString, endsWith, startsWith (para string) é possível combinar vários ViewMatchers:

onView(                                          //Procura uma View
    allOf(                                       //Que atenda todos os critérios:
        not(withText(containsString(”Teste”))),  //Não contenha o texto “Teste”
        isDisplayed(),                           //Esteja na tela
        isAssignableFrom(TextView.class)         //Cuja classe é TextView
    )
);

Dois aspectos importantes devem, porém, ser considerados:

Primeiro: o método onView (e onData) espera que uma e somente uma View atenda aos critérios que você passa, do contrário, será lançada uma exceção “AmbiguousViewMatcherException”. Mas não se desespere, o stack trace do erro é bastante completo, mostrando as Views do layout atual e quais são as que atendem aos critérios passados.

Segundo: sobre o isDisplayed() e isCompletelyDisplayed(). Eles irão falhar caso a View esteja no layout mas não esteja visível para o usuário (pode estar até, por exemplo, escondida pelo teclado). E a diferença dos dois métodos, que é simples, isDisplayed encontra a View mesmo que só uma parte dela esteja na tela, enquanto isCompletelyDisplayed, como diz o próprio nome, busca as que estão completamente visíveis na tela.

E o que é o onData, afinal?

Bom, esse método é para ser utilizado com AdapterViews (mais comuns são Spinners e ListViews) já que não são infladas todas as Views da lista, mas somente que as são efetivamente mostradas ao usuário. O restante somente é carregado se o usuário der um scroll na lista. Assim, não é possível utilizar o método onView, já que este requer que algo que esteja no layout. Caso tenha poucos itens na lista, talvez você até consiga utilizar o onView, mas, em geral, é mais confiável e recomendável utilizar o onData.

A utilização deste método normalmente é bem simples, desde que você atente ao fato de que deve-se procurar o dado em si, por exemplo uma String, no caso mais simples, ou um determinado objeto, e não a view que contém essa String, ou os dados desse objeto. Para chegar até um item “Teste” de um determinado ListView, utilizando o onData:

onData(
    allOf(
        is(instanceOf(String.class)),
        is(“Teste”)
    )
);

Alguns cuidados a serem tomados com o onData: caso você tenha mais de um AdpterView na tela, como um Spinner e uma ListView, é preciso especificar qual deles através do método inAdapterView, que funciona de forma similar ao onView:

onData(
    allOf(
        is(instanceOf(String.class)),
        is(“Abc123”)
    )
).inAdapterView(
    withId(R.id.spn_teste)
);

Agora que você já viu como localizar as Views na tela, deve se perguntar, e o que eu posso fazer com isso?
Basicamente, verificar coisas, através do método check, e realizar ações, através do método perform.

Vamos primeiro às verificações. O método check recebe como parâmetro uma ViewAssertion. Existem poucas implementadas, provavelmente as duas que mais serão utilizadas serão doesNotExist e matches. A primeira é fácil, verifica que a View não existe na tela (ou seja, não está presente no layout).

A função matches, por sua vez, é similar à chamada da onView, ela recebe, também, um objeto ViewMatcher, ou seja, tudo aquilo que você podia utilizar para encontrar uma View, pode utilizar para verificar parâmetros dela:

onView(
    withId(R.id.meu_id)
).check(
    matches(
        allOf(
            isDisplayed(),
            withText(“Texto Teste”)
        )
    )
);

É verificado a se a View com id “meu_id” é mostrada na tela e contém o texto “Texto Teste”.

Importante: tanto o método onView quando o método matches aceitam um ViewMatcher como parâmetro, mas eles possuem propósitos distintos! Por exemplo, se você quiser verificar que uma View com um determinado ID é mostrada na tela, o seguinte código não seria uma boa prática:

onView(
    allOf(
        withId(R.id.meu_id),
        isDisplayed()
    )
);

Já que, nesse caso, ao utilizar essa ViewInteraction, mesmo que exista uma View com o ID especificado, se ela não estiver na tela, resultará em uma NoMatchingViewException. O correto seria:

onView(
    withId(R.id.meu_id)
).check(
    matches(
        isDisplayed()
    )
);

O que levaria a um assertion fail, dizendo que a View em questão não está visível, que é o que efetivamente queríamos testar.

O primeiro código seria correto apenas no caso de, por exemplo, termos várias Views com aquele ID, mas apenas uma delas estar visível. Aí, poderíamos verificar que o texto dessa View é “abc”:

onView(
    allOf(
        withId(R.id.meu_id),
        isDisplayed()
    )
).check(
    matches(
        withText(“abc”)
    )
);

Quanto às ações que podem ser realizadas nas Views, há várias possibilidades: click, scrollTo, typeText, clearText, para citar algumas. Essas ações devem ser passadas através do metodo perform e é possível passar várias ações que serão executadas em sequência:

onView(
    withId(R.id.txt_nome)
).perform(
    scrollTo(),
    clearText(),
    typeText(“Nome Sobrenome”)
);

Um outro aspecto positivo, que talvez alguns discordem e digam que é negativo, é que, ao utilizar o Espresso, você deve começar a pensar como o usuário do programa para montar os testes.

O que isso significa na prática? Que você não vai conseguir digitar um texto em uma View que está no fim da tela sem dar um scroll, ou clicar naquele botão que o teclado tapou sem fechar o teclado. Sim, às vezes pode ser inconveniente caso você tenha que fazer isso seguidamente. A questão é: o usuário vai ter que fazer a mesma coisa para utilizar essa tela. Pois é. Talvez seja hora de rever o layout.

Caso não seja possível alterar, bom, você sempre tem a liberdade de implementar suas próprias ViewActions. É possível, por exemplo, criar uma versão do typeText que sempre dê um scroll antes de efetivamente escrever o texto. Ou uma implementação do próprio perform que faça isso.

Ainda nessa questão de pensar como o usuário, para os fãs de TDD (não sabe o que é TDD?), utilizando o espresso é inclusive possível definir vários quesitos do layout na implementação do teste. Por exemplo: você está fazendo o teste para uma tela com um formulário para preencher alguns dados. Essa tela deve receber um endereço de e-mail. Nada mais lógico do que o campo onde esse e-mail deve ser digitado estar próximo de uma TextView com um texto “E-mail”, não é mesmo?

onView(
    withId(R.id.txt_email)
).check(
    matches(
        isDisplayed(),
        hasSibling(
            allOf(
                withText(“E-mail”),
                isAssignableFrom(TextView.class)
            )
        )
    )
);

O ViewMatcher hasSibling procura uma View que atenda aos critérios passados (TextView com texto “E-mail”) entre as Views do mesmo grupo da View atual. Ou seja, se você tiver, por exemplo, se a view de id “txt_email” estiver dentro de um LinearLayout, ele vai procurar a TextView dentro desse LinearLayout.

Assim como é possível implementar ViewActions próprias, também é possível implementar ViewMatchers e ViewAssertions (para utilizar com o método check). Para isso, é bom se familiarizar com o código do Espresso, que pode ser obtido clonando o repositório git: https://code.google.com/p/android-test-kit/.

Legal, legal, parece bom, mas não tem nenhum aspecto negativo nisso tudo?

Sim, existem algumas questões puntuais na utilização do espresso. Primeiramente, se trata de uma tecnologia recente e pouco difundida ainda. Há poucas questões em fóruns e afins sobre o Espresso. Caso você tenha algum problema na utilização pode ser um pouco difícil inicialmente de achar uma solução. Há, também, poucos tutoriais, mesmo em inglês.

Por outro lado, com uma maior utilização da API, é provável que vá surgir cada vez mais documentação a respeito, uma coisa depende da outra. Além do mais, trata-se de um projeto código aberto, é sempre possível buscar a razão de uma Exception estranha no próprio código. Mas em geral a stack trace do erro já é mais do que suficiente.

Existe, também, uma questão de incompatibilidade com o PowerMock. Não é possível utilizar os dois no mesmo teste, pelo menos não de maneira simples, pois cada um tem um TestRunner próprio.
Para quem usa o Mockito, não há problemas, só é necessário tomar cuidado na instalação pois as duas bibliotecas utilizam os Hamcrest Matchers, de modo que é necessário pegar ou o espresso separado com as dependências e não adicionar a biblioteca hamcrest, ou fazer isso com o Mockito (recomendo fazer com o mockito, que possui apenas 3 jars, se pegar do espresso, são 14).

Outro ponto é sobre a legibilidade do código depender de alguns “import static”, mas se você usa uma biblioteca de mocks já deve estar acostumado. E, além do mais, depois de um ou dois testes, conhecendo um pouco melhor a API, isso já não é mais um grande problema.

Não entendeu nada? Basicamente, um código só importando a biblioteca do Espresso ficaria desse jeito:

Espresso.onView(
    Matchers.allOf(
        ViewMatchers.withId(R.id.meu_id),
        ViewMatchers.withText(“Teste”)
    )
).check(
    ViewAssertions.matches(
        ViewMatchers.isDisplayed()
    )
).perform(
    ViewActions.swipeLeft(),
    ViewActions.click()
);

Ruim, né?

Utilizando “import static” nos métodos utilizados, o código fica assim:

import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView;
import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.click;
import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.swipeLeft;
import static com.google.android.apps.common.testing.ui.espresso.assertion.ViewAssertions.matches;
import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isDisplayed;
import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId;
import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withText;

...

onView(
    allOf(
        withId(R.id.meu_id),
        withText(“Teste”)
    )
).check(
    matches(
        isDisplayed()
    )
).perform(
    swipeLeft(),
    click()
);

Bem melhor.

Por fim, se você está acostumado a utilizar Toasts para aviso ao usuário, infelizmente a verificação destes não é suportada pelo Espresso, pois não é possível manter o sincronismo com esse tipo de notificação, não é possível garantir que o Toast esteja visível ainda no momento que você fizer o teste. Também não há previsão de que isso seja revisto.

A boa notícia é que é possível contornar esse problema utilizando uma classe singleton e, por exemplo, com o Mockito, utilizando um spy e o método verify:

public class ToastFactory {
    private static ToastFactory instance;

    public static void setInstance(ToastFactory mockInstance){
        instance = mockInstance;
    }

    public static ToastFactory getInstance(){
        if(instance == null) return new ToastFactory();
        return instance;
    }

    public Toast makeText(Context context, CharSequence text, int duration){
        return Toast.makeText(context, text, duration);
    }

    public Toast makeText(Context context, int resId, int duration){
        return Toast.makeText(context, resId, duration);
    }
}

As chamadas passam a ser, então:

ToastFactory.getInstance().makeText…

No setUp dos testes, inicialize o spy:

ToastFactory toastSpy;
…

toastSpy = spy(new ToastFactory());
ToastFactory.setInstance(toastSpy);

Toast toastToReturn = spy(Toast.makeText(mTargetContext, "Teste", Toast.LENGTH_SHORT));

doReturn(toastToReturn).when(toastSpy).makeText(any(Context.class), anyString(), anyInt());
doReturn(toastToReturn).when(toastSpy).makeText(any(Context.class), anyInt(), anyInt());

E, por fim, para verificar, utilize o método verify:

verify(toastSpy, times({quantidade de vezes})).makeText(any(Context.class), eq({String resource ID ou texto}), anyInt());

Vale ou não a pena implementar?

Pela linha de raciocínio durante o post, você deve saber que vou recomendar a utilização, mas cabe a cada um analisar o projeto em si e verificar se os pontos negativos serão um impedimento ou não na implementação. O ideal é, se você já utiliza outra API para testes de interface, implementar alguns destes testes com a API do Espresso e fazer algumas comparações.

Em geral, a execução dos testes é bem mais rápida devido à sincronia com as chamadas da interface, não dependendo de loops esperando por uma determinada condição, ou pior, de sleeps, que normalmente atrasam a execução do teste, muitas vezes sem necessidade.

Além disso, como já citado, reduz drasticamente a chance de um mesmo teste rodando em máquinas diferentes apresentar problemas somente em uma delas, ou até mesmo aquele teste que sempre passou e ninguém mexeu de repente começar a falhar sem motivo aparente.

Quer começar e não sabe por onde?

https://code.google.com/p/android-test-kit/wiki/Espresso#Getting_Started
Nessa página você encontra um tutorial de como configurar o Espresso e o Google Instrumentation Test Runner em um projeto tanto no Eclipse quanto no Android Studio.

http://code.google.com/p/android-test-kit/wiki/EspressoSamples
Exemplos mais completos e complexos que podem ser utilizados como referência.

https://code.google.com/p/android-test-kit/
https://code.google.com/p/android-test-kit/source/browse/espresso/
Código fonte (repositório GIT que pode ser clonado ou acesso pelo navegador), além de mais alguns projetos de testes.

Gostou do post, tem uma dúvida, achou o Espresso legal, achou horrível, conseguiu fazer funcionar com o PowerMock? Comenta aí.

Escrito por Rodrigo Lessinger

Visão geral do Windows Workflow Foundation

O Windows Workflow Foundation, integrado ao .NET Framework 3.0 em 2006, é uma tecnologia desenvolvida pela Microsoft que fornece um conjunto de bibliotecas para auxiliar no desenvolvimento de fluxos de trabalho de maneira rápida e fácil, oferecendo uma linguagem de mais alto nível para criação de tarefas paralelas e assíncronas, além de fornecer suporte a escalabilidade das aplicações.

Como desenvolver aplicações simples e escaláveis?

Uma maneira de desenvolver uma aplicação simples é criar uma única aplicação que seja executada em um único processo em uma única máquina. Com essa abordagem, o código torna-se mais fácil de ser entendido e a ordem de execução de cada passo é bem definida pelo fluxo. O estado da aplicação também pode ser acessado facilmente, já que é armazenado em uma variável. Porém, quando a aplicação precisa esperar por alguma entrada, a thread e o processo ficam bloqueados até que a entrada seja fornecida. Dado esse consumo de threads e processos, a aplicação não escala com grandes cargas de trabalho.

Uma alternativa para tornar a aplicação escalável é quebrá-la em vários blocos, que possam ser executados em threads diferentes em máquinas diferentes. No entanto, o código torna-se mais complexo e a ordem de execução de cada passo menos clara, já que a lógica está espalhada pelos diversos blocos. Além disso, o estado da aplicação precisa ser armazenado externamente, como em um banco de dados.

Para superar essas dificuldades, foi criado o Windows Workflow Foundation (WF). O principal objetivo da ferramenta é escrever aplicações escaláveis, que estejam preparadas para manipular grandes cargas de trabalho sem consumir muitos recursos, e fáceis de serem entendidas por todos, desde desenvolvedores até profissionais responsáveis pela manutenção.

Então, como funciona o Windows Workflow Foundation?

No WF, um fluxo de trabalho (workflow) é uma série de passos, cada um modelado por uma atividade (activity), que pode ser a leitura de um dado de entrada, a exibição de um dado de saída, controle de fluxo, execução de código, entre outros. Uma atividade pode ser também composta por outras, como a atividade chamada sequência (sequence), que pode armazenar variáveis e executar outras atividades, como condicionais (if) e laços (while). As atividades podem ser implementadas utilizando as demais bibliotecas .NET, como System, WCF e WPF. Além disso, o WF também fornece um framework para suporte a implementação de testes unitários e integrados.

A figura abaixo, retirada de um artigo do autor David Chappell, mostra um exemplo de um fluxo de trabalho, à direita, comparando-o com o código correspondente à esquerda. A execução do fluxo é feita pelo WF Runtime, um componente responsável por executar as atividades. O WF oferece ainda a Base Activity Library (BAL), onde podem ser encontradas atividades básicas como WriteLine, para exibir uma mensagem na tela.

Além de fornecer um controle único de fluxo, tornando o código mais fácil de ser compreendido, o WF também oferece escalabilidade. Quando a aplicação está esperando por um dado de entrada, o fluxo é bloqueado. Então, o WF Runtime armazena o estado da aplicação e o ponto de onde o fluxo deve continuar, encerrando o fluxo. Quando a entrada é fornecida, o WF Runtime recupera o estado e recomeça o fluxo do ponto armazenado. O WF Runtime é o único responsável por esse mecanismo, não sendo necessária nenhuma ação do desenvolvedor. Além de não bloquear recursos, esse mecanismo permite que diferentes blocos do fluxo de trabalho possam ser executados em diferentes máquinas.

Por que usar o Windows Workflow Foundation?

Além das vantagens já mencionadas, outros benefícios do WF são:

• Como o WF Runtime espera pelo dado de entrada quanto tempo for necessário, é uma excelente ferramenta para processos de longa duração;

• Suporte ao parelelismo e tarefas assíncronas de maneira simples, manipulando essas tarefas através do WF Runtime, sem necessidade de interferência do desenvolvedor;

• Criação de atividades personalizadas, que podem ser reusadas em todo o fluxo;

• Visualização gráfica do fluxo de trabalho, descrita em XAML.

Então, o que achou do Windows Workflow Foundation? Qualquer dúvida ou sugestão, deixe nos comentários.

Escrito por Paula Burguêz

Agile é bom para quem?

Já falamos aqui no blog sobre o quão úteis metodologias ágeis são para o desenvolvimento (de sistemas e das pessoas). Elas contribuem muito para a maturidade das equipes, resultando em códigos cada vez mais qualificados e em projetos que entregam mais valor em menos tempo. As equipes ficam mais motivadas, pois participam ativamente das decisões, são auto-organizadas e uma série de outros benefícios que a maioria de nós já conhece.

Porém, migrar de projetos de escopo fechado ou cascata para o uso de Agile não é tarefa fácil para a empresa. “Junto com grandes poderes, vem grandes responsabilidades.” Quem não conhece essa máxima clássica do tio Ben Parker, do nosso amigo Homem Aranha? Com Agile é a mesma coisa. Além do desafio de vender a ideia dentro da empresa, enfrentar resistências (e alguns desvios) e provar que funciona, a área de negócios tem o grande desafio de estruturar uma forma de vender para o cliente, pois o uso dessas metodologias exige, antes de tudo, confiança. Trata-se de uma quebra de paradigmas muito grande. Porém, depois de iniciado o processo, o ganho é tão grande que o próprio cliente tende a pensar como ele não contratou fornecedores assim antes.

Para começar, ao contrário de um projeto de escopo fechado ou waterfall, a prioridade é pela construção de funcionalidades realmente… funcionais, úteis, essenciais. O foco é na entrega daquilo que possui maior valor, evitando o escopo fechado com aqueles requisitos mega detalhados que não serão utilizados nunca. Com isso, os desperdícios são drasticamente reduzidos, sejam eles de custos, de prazos ou dos cabelos brancos da equipe, que muitas vezes pode atropelar a qualidade para cumprir prazos irreais.

O problema, porém, é que muitas vezes há a falsa ideia de que o fornecedor de TI fará o que quer, quando quiser e um projeto pode começar com uma ideia de 100 mil e acabar custando um milhão. Isso não é bem assim… tudo começa com base em uma estimativa inicial, um projeto base. O cliente determinada que item ele não pode mexer. Por exemplo: A verba para isso é de xx e não pode ser ultrapassada OU o prazo para isso é xx. O que pode ser feito nesse tempo?

Enfim, Agile implica em transparência. Todos sabem o que está acontecendo o tempo todo e as decisões são tomadas por um time, visando benefícios para todas as partes, com alta qualidade, redução de custos e aumento do valor agregado do produto. Em um próximo post, vamos entrar mais a fundo na metodologia SCRUM, os diversos papéis e como iniciar o primeiro projeto com o uso dessa metodologia.

Escrito por Aline Roque

Testes unitários e TDD – Conceitos Básicos

Quando se fala de TDD (Test-Driven Development), um conceito relativamente novo (ou que pelo menos atraiu mais interesse ultimamente), devemos entender primeiramente o que são os testes e como eles se encaixam no processo de TDD

Para novatos (newbies ou jovens Padawans) no TDD existe uma certa confusão na diferenciação entre TDD e os testes em si.

Natural, visto que o TDD anda de mãos dadas com testes automatizados, e os tem como o centro da metodologia.

O que vale ressaltar é que TDD é o processo em si (sequência de passos) ou a metodologia.

É um conceito intangível, enquanto os testes são o objeto e o centro desse processo, e não existem apenas no plano das ideias.

Existem diversos tipos de testes, como testes de eficiência, testes de stress, teste de fidelidade, etc., mas não os abordaremos neste post.
Abaixo trataremos dos testes de unidade ou unitários, que compõem o cerne da metodologia TDD (a qual veremos mais abaixo no post).

Teste Unitário

O teste unitário tem por objetivo testar a menor parte testável do sistema (unidade), geralmente um método.

Idealmente, o teste unitário é independente de outros testes, validando assim cada parte ou funcionalidade individualmente.

Nos casos em que existe dependência de métodos ou classes que não estão sob teste, são usados mocks (objetos criados para simular o comportamento de objetos reais nos testes) para simular estes objetos ou métodos que são usados pela unidade, desacoplando a unidade sob teste dos componentes dos quais ela é dependente.

Os testes unitários tem como benefício:

  • Garantir que problemas serão descobertos cedo, ainda durante o processo de desenvolvimento, devendo ser atualizados conforme novas funcionalidades se tornarem necessárias (veremos adiante como o TDD torna isto natural).
  • Facilitar a manutenção de código, especialmente a refatoração, porque indica se a unidade ainda está funcional após as alterações e aponta erros, caso existam.
  • Simplificar a integração de módulos, visto que minimiza erros de unidade.
  • Servir como documentação, dando detalhes da funcionalidade e implementação da unidade.

Exemplo

Abaixo temos um método simples para validação de formato de números de telefone.

public class PhoneValidator {
    public boolean isValid(string phone)
    {
          return useSomeRegExToTestPhone(phone);
    }
}

E o método para testa-lo…

public class PhoneValidatorTest {
    public void testIsValid() {
    	String goodPhone = "(123) 555-1212";
    	String badPhone = "555 12"

    	PhoneValidator validator = new PhoneValidator();

     	assert.isTrue(validator.IsValid(goodPhone));
     	assert.isFalse(validator.IsValid(badPhone));
    }
}

Quando escrevemos um teste unitário para um método, devemos cobrir todas as possíveis saídas do mesmo.

No teste acima, cobrimos os dois casos principais do método sob teste: verdadeiro e falso.

Poderiamos ter feito isso em dois métodos de teste separados, com assinaturas TestPhoneValidatorTrue e TestPhoneValidatorFalse, obtendo o mesmo resultado.

No caso, não precisamos simular o comportamento de nenhum método ou classe, porque teoricamente o método useSomeRegExToTestPhone seria um método da própria classe sendo testada e também porque mantivemos a dependência do método isValid para o método useSomeRegExToTestPhone. Mas como faríamos se quiséssemos eliminar essa dependência e testar apenas o método isValid independentemente da funcionalidade de useSomeRegExToTestPhone?

public class PhoneValidator
{
    private CustomRegexValidator regexValidator;

    public PhoneValidator(CustomRegexValidator regexValidator) {
    	this.regexValidator = regexValidator;
    } 

    public bool isValid(string phone)
    {
          return regexValidator.useSomeRegExToTestPhone(phone);
    }
}

Acima, o método useSomeRegExToTestPhone faz parte da classe CustomRegexValidator.

Como não temos a intenção de testar a funcionalidade desta classe, que poderia até mesmo fazer parte de uma biblioteca terceira, “mockamos” uma instância da classe e simulamos os resultados que queremos do método useSomeRegExToTestPhone, conforme o código abaixo:

public class PhoneValidatorTest {
  private CustomRegexValidator regexValidator;

  public PhoneValidatorTest {
     regexValidator = mock(CustomRegexValidator.class);
  }

  public void testIsValid () {
     String goodPhone = "(123) 555-1212";
     String badPhone = "555 12"

     when(regexValidator.useSomeRegExToTestPhone(goodPhone)).thenReturn(true);
     when(regexValidator.useSomeRegExToTestPhone(badPhone)).thenReturn(false);

     PhoneValidator validator = new PhoneValidator(regexValidator);

     assert.isTrue(validator.isValid(goodPhone));
     assert.isFalse(validator.isValid(badPhone));
  }

Escrevendo o teste unitário desta forma, estamos isolando o método isValid e garantindo que ele funcione como esperado.

Os métodos mock e when fazem parte do framework de testes Mockito para Java, mas alternativas de frameworks para testes existem em qualquer linguagem.

Quando escrevemos nossos testes, devemos sempre ter o mesmo zelo pela qualidade deles que temos (ou deveríamos ter) para com o código em si.

Portanto, podemos refatorar os testes acima para abranger mais casos e evitar duplicação, vide código abaixo:

public void testIsValid_CorrectNumbers() {
     String[] goodPhones = new String[]{
                         "(123) 555-1212",
                         "555-1212",
                         ...
                         "5551212"
                        };

     assertReturnValuePhoneNumbers(goodPhones, true);
  }

  public void testIsValid_InCorrectNumbers() {
     String[] badPhones = new String[]{
                         "(-) 555-1212",
                         "dfs555-1212",
                         ...
                         "+--5551212"
                        };

     assertReturnValuePhoneNumbers(badPhones, false);
  }

  private void assertReturnValuePhoneNumbers(String[] phoneNumbers, bool expectedReturnValue) {
     for(String phoneNumber :phoneNumbers) {
         when(regexValidator.useSomeRegExToTestPhone(phoneNumber))
                            .thenReturn(expectedReturnValue);

         PhoneValidator validator = new PhoneValidator(regexValidator);

         assert.equals(expectedReturnValue, validator.isValid(phoneNumber));
      }
  }

A seguir, veremos os conceitos da metodologia TDD.

TDD

Test-Driven Development ou Desenvolvimento Orientado a Testes é uma técnica de desenvolvimento de software em que, ao contrário do que em geral estamos acostumados, primeiro são escritos os testes e só depois é produzido um código que possa validá-lo.

O ciclo de desenvolvimento do TDD

Ciclo de desenvolvimento com TDD
A base para o TDD é um ciclo curto de desenvolvimento, que pode ser mostrado utilizando como exemplo um método que, dado um número romano, retorna o seu valor, ilustrado no livro “Test-Driven Development – Teste e Design no mundo real” escrito por Mauricio Aniche.

   public class ConversorNumeroRomano {
      public int converte(String numeroRomano) {
         return 0;
      }
   }

1. Escreva um teste para uma nova funcionalidade desejada
A primeira funcionalidade que desejamos desse método, é que ele deve retornar 1 quando o algarismo I é passado como parâmetro. Esse, então, é o primeiro caso de teste.

public void testConverteNumeroRomano_DeveConverterOAlgarismoI(String numeroRomano){
      ConversorNumeroRomano conversor = new ConversorNumeroRomano();

      //O método deve retornar 1 quando é passado o algarismo I
      int valor = conversor.converte("I");
      Assert.equals(1, valor);
}

2. Verifique que o teste falha.
Com certeza o teste irá falhar, pois nada foi implementado. Inicialmente pode não parecer, mas esse passo é importante para que, ao longo do desenvolvimento, tenhamos certeza de que o teste foi codificado corretamente e é capaz de encontrar erros.

3. Escreva o código necessário para que o teste seja validado
O TDD exige que seja escrito código somente para validar o teste, ou seja, não devemos nos preocupar em implementar o método para qualquer caso, ou qualquer outra funcionalidade para a qual não se tenham testes
ainda. Assim, simplesmente retornando 1 resolvemos o problema:

public int converte(String numeroRomano) {
      return 1;
}

4. Execute o teste e verifique que o teste agora passa.
Executando o teste, verificamos que ele não vai falhar agora, pois retornamos exatamente o que ele espera.

5. Sabendo que o código atende a funcionalidade desejada, refatore o código.
Nesse caso o código ainda está bastante simples, apenas retorna 1, não é necessário refatorar, portanto podemos voltar ao passo 1 e escrever um novo teste.

Em seguida, poderíamos testar para cada um dos algarismos romanos simples, o que poderia ser resolvido inicialmente com uma condição, depois refatorado para um switch.

Então, seria necessário considerar os casos em que há mais de um algarismo, por exemplo II, XI, LXXVI, etc., em que poderíamos, então, processar a entrada caractere a caractere, somando os valores.

Como não é possível considerar todos os casos de teste possíveis, o ideal é testar pelo menos todos os casos limites ou “diferentes”, por exemplo, todos os algarismos simples, algarismos grandes, combinações que os algarismos são subtraídos e não somados, para que seja possível obter um algoritmo funcional.

Deve-se lembrar também de testar casos que não devem ser válidos, como, nesse caso, números romanos com algarismos inválidos, ou casos em que um algarismo aparece mais de três vezes seguidas. Nestes casos, pode-se, por exemplo, esperar que uma exceção seja lançada. Dessa forma, também é possível definir alguns comportamentos do código em questão de antemão.

Por que utilizar o TDD?

Pode parecer muito básico ou sem sentido no começo, por exemplo, ao simplesmente retornar o valor 1 no primeiro caso de teste, mas essa técnica nos ajuda a resolver o problema passo a passo, implementando as necessidades conforme for necessário.

Assim, num primeiro momento, não precisamos nos preocupar com todos os casos possíveis, isso se dá forma gradual a cada teste implementado, evitando que se perca muito tempo quebrando a cabeça para resolver um problema complexo quando ele pode ser resolvido em partes e refatorado.

Além disso, ao implementar o teste primeiro, pensando apenas na funcionalidade que desejamos implementar, evitamos de nos basear no próprio código para montar os testes, esquecendo algumas vezes de alguns casos que não foram previstos inicialmente.

Escrevendo os testes e definindo o comportamento dos métodos antes, como no exemplo anterior em que é esperada que seja lançada uma exceção já pré-definida, permite que outros desenvolvedores saibam como o código vai se portar antes mesmo dele ser escrito, além de ser possível definir os requisitos do projeto ou da funcionalidade para o desenvolvedor.

Pode-se pensar como um contraponto do TDD, ou aos testes unitários em si, o tempo e o trabalho necessário para desenvolver os testes, porém, esse tempo “perdido” é recompensado mais para frente, quando é necessário refatorar um código ou procurar aquele bug que resolveu aparecer dois anos depois naquele código que ninguém mexe faz séculos.

Em alguns casos, os testes unitários praticamente substituem a função de um debugger, já que, modificando o código e verificando quais dos testes deles falham, é possível saber onde exatamente está o problema.
Um outro ponto muito importante, que diz respeito também aos testes unitários, é com relação à posterior manutenção do código. Não é necessário se preocupar com o fato de que possíveis refatorações ou alterações resultem em bugs novos e inesperados, já que todas as funcionalidades do sistema estão sendo testadas.

Dessa forma, acaba o medo de refatorar aquele trecho pré-histórico que ninguém mais sabe de onde surgiu, ou mesmo de codificar uma funcionalidade nova que pode alterar o funcionamento de outras antigas, já que se surgir algum imprevisto, algum teste irá falhar, e muitas vezes só de olhar qual foi o teste que falhou já é possível identificar o erro. Além do fato de que ninguém pode pôr a culpa se algum bug que surgir em algum código seu, já que, quando você desenvolveu, todos os testes passaram.

Introduzida a teoria sobre o que é TDD e seus os benefícios. Em breve botaremos a mão na massa, com um novo post, sobre como iniciar um projeto básico aplicando o TDD na prática.

Escrito por Luciano Luzzi e Rodrigo Lessinger

Na onda das retrospectivas, que tal relembrar os melhores momentos do blog?

Estamos em 2014 e isso significa que nosso blog está entrando no seu terceiro ano de atividades. E para que esse terceiro ano seja de primeira, resolvemos parar e olhar um pouco para trás, revendo nossa história para planejar os próximos passos.

Por isso, compartilhamos com vocês um pouco da nossa trajetória até aqui. Confira!

Foram publicados mais de 100 posts, na grande maioria sobre assuntos técnicos abordando o universo do desenvolvimento mobile. Para que eles fossem produzidos, 27 colaboradores da Mobiltec disponibilizaram seu tempo para gerar um conteúdo de qualidade para você. Parece que foi bacana, pois o blog recebeu mais de 120 mil visitantes, que visualizaram mais de 220 mil vezes nossas páginas. Dentre os temas mais abordados, estão os relacionados ao nosso dia a dia de desenvolvimento, como a tecnologia mobile device management (ou MDM), html5, desenvolvimento Android, iOS, Windows Phone, multiplataforma, phonegap, metodologias ágeis e muito mais!

Os três posts mais lidos:

3º lugar: Desenvolvimento Mobile nas Plataformas Android e iOS

2º lugar: Desenvolvimento Mobile Multiplataforma com Phonegap

1º lugar: Tutorial Android – Parte 1 – Primeiros Passos

Os três posts mais comentados:

3º lugar: Tutorial Android – Parte 1 – Primeiros Passos

2º lugar: Desenvolvimento Mobile Multiplataforma com Phonegap

1º lugar: Tutorial de Phonegap – Primeiros Passos

Para 2014, as postagens vão começar um pouco mais espaçadas, mas mantendo a qualidade de sempre.

Nossos blogueiros, digo, desenvolvedores, vão explorar assuntos de alto nível, incluindo posts conceituais sobre temas emergentes, como cloud, metodologias ágeis (com práticas) além dos bons e velhos posts técnicos com aquele código amigo que ajuda tanta gente! Também faremos reviews sobre os temas mais lidos, que tenham sofrido atualizações significativas desde a época da postagem. E se você tiver alguma sugestão de tema, envie pra gente! Esse ano tem tudo para ser, como muitos diriam, DEMÓIS! Aguarde e confie!