Testes automatizados: colocando o TDD em prática

Nós já abordamos o tema TDD aqui no blog outras vezes, explicando como o uso dos testes podem melhorar a performance da sua aplicação e diminuir possíveis falhas. No post anterior, explicamos o conceito do Test-driven development. Agora, vamos por a mão na massa, mostrando a aplicação prática do TDD no Android. Um ótimo começo para quem deseja se aventurar nos primeiros passos com o uso de TDD.

Sempre lembrando:

O que precisamos?

Utilizaremos um projeto Android de exemplo, um projeto de testes do Android, um framework para criação e configuração de mocks e um framework para testes de interface.

Mocks

Quando estamos lidando com testes unitários, devemos evitar influência externa de outras classes, por exemplo, um teste que depende de uma coordenada fornecida via GPS ou de dados da Internet não pode acusar uma falha no teste caso esses componentes falhem, sem relação com a aplicação.

Esse isolamento pode ser um tanto complexo de implementar, considerando que classes dependem de outras classes e eventualmente lidamos apenas com interfaces, sem nem saber qual a classe que afinal está sendo usada.

Portanto, para evitar esses problemas e fazermos testes consistentes, utilizamos mock objects, objetos que simulam o comportamento de objetos reais e que podem ser configurados, isto é, com os quais podemos fazer com que os métodos retornem o que bem entendermos, quando bem entendermos.

Utilizaremos o framework Mockito (é tão bom que tem referência à bebida no nome), que pode ser encontrado aqui: https://code.google.com/p/mockito/

Criamos um projeto de teste do Android, e incluimos na pasta libs as seguintes dependências:

Observação: não utilizamos a biblioteca com todas as dependências pois resultaria em um conflito com o framework dos testes de Interface.

Exemplo:

 import static org.mockito.Mockito.*;
import com.exemploMock.model.Usuario;

public class Teste {
	public void test() {
		Usuario usuarioMock = mock(Usuario.class);
	}
}

Acima, estamos criando um objeto mock da classe UsuarioModel.
A partir desse objeto mock, podemos configurar o retorno dos métodos, conforme segue:

when(usuarioMock.getNomeUsuario()).thenReturn("José");

Acima, estamos criando um objeto mock da classe UsuarioModel.

A partir desse objeto mock, podemos configurar o retorno dos métodos, conforme segue:

when(usuarioMock.getNomeUsuario()).thenReturn("José");

Ao longo do resto do post, veremos melhor a aplicação do mockito.

Espresso

Para testes de interface, utilizaremos o já citado Espresso.

Sugerimos que utilize o kit de testes do Android.

Exemplo:

 public class Teste {
	public void test() {										onView(withId(R.id.txtUsuario))
.check(matches(isDisplayed()))
.perform(typeText(“ABC”));

onView(withText(“Confirmar”)).perform(click());
	}
}

Com esse trecho de código, confirmamos que o campo usuário está visível, digitamos “ABC” e clicamos em um botão “Confirmar.

Iniciando um projeto

Montando a Interface com TDD

Ok, agora que já temos nossas armas, podemos começar.

Criaremos um projeto básico com uma tela de login (não sabe criar um projeto Android?).

 

E seu respectivo projeto de teste (File -> New -> Other…, Android -> Android Test Project) referenciando o projeto original.

Adicionamos, por fim, as referências citadas acima na pasta lib do nosso projeto de teste.

E estamos prontos para começar!

Para fins de exemplo, nossa aplicação vai ser uma tela simples de login, com dois EditTexts (um para o nome de usuário e outro para a senha), dois TextViews que indicarão para o usuário da finalidade dos campos, e um Button para o realizar o Login.

O primeiro passo no ciclo do TDD é a criação do teste. Começaremos, então, criando a classe de teste, chamada LoginActivityTest.

As classes que testam Activities devem sempre ser subclasses da classe ActivityInstrumentationTestCase2, onde o tipo genérico T é o tipo da Activity que queremos testar. Devemos, também, fornecer um construtor sem argumentos, conforme segue:

 public class LoginActivityTest
extends ActivityInstrumentationTestCase2 {

	public LoginActivityTest() {
		super(LoginActivity.class);
	}
}

Código que, obviamente, não compila, pois não criamos ainda a classe LoginActivity. Seguimos então o ciclo do TDD. Fazemos o mínimo possível para que resolvamos esse problema, ou seja, criamos a classe:

 

Agora, com os projetos compilando, rodamos os testes e… tudo certo! Nenhum teste falhou (e nenhum passou também, mas isso é apenas um detalhe).

Vamos adiante. Começaremos agora, especificando no teste que queremos que exista uma View correspondente ao nosso campo de usuário e que ela esteja visível na tela.

public class LoginActivityTest
extends ActivityInstrumentationTestCase2 {

public void testLoginActivity_Interface(){
		getActivity();

		onView(withId(R.id.txt_usuario))
			.check(matches(isDisplayed()));
}
}

A chamada para o método getActivity() deve ser feita sempre antes de qualquer operação na view, pois ela é responsável por iniciar efetivamente a Activity.

Agora, rodando o projeto de testes, temos um teste falhando. Vamos resolver, mais uma vez, codificando o mínimo possível. Criaremos o arquivo de layout da nossa activity (chamado login_activity.xml) na pasta de recursos do projeto principal e declaramos a EditText.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

	<EditText
	    android:id="@+id/txt_usuario"
	    android:layout_width="match_parent"
	    android:layout_height="wrap_content"
	    android:inputType="text" />

</LinearLayout>

Ainda temos que adicionar o layout na Activity, se rodarmos o teste agora vamos ter um erro (pode testar).

 public class LoginActivity extends Activity {

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);

		setContentView(R.layout.login_activity);
	}
}

Novamente, temos todos os testes passando. Podemos seguir adiante.

Vamos especificar agora uma TextView que indicará a finalidade do campo que criamos.

public class LoginActivityTest
extends ActivityInstrumentationTestCase2 {

public void testLoginActivity_Interface(){
		getActivity();

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

		onView(withId(R.id.lbl_usuario))
			.check(
				matches(
					allOf(
						isDisplayed(),
						withText(R.string.lbl_usuario),							hasSibling(
withId(R.id.txt_usuario)
)
					)
				)
			);

}
}

Nesse segundo teste, fazemos mais algumas especificações quanto à View. Verificamos, novamente, que ela está visível, mas queremos também que ela contenha um determinado texto (uma string “Usuário”). Além disso, o ViewMatcher hasSibling verifica se ela está próxima do nosso campo de usuário (verifica se os dois tem o mesmo layout como pai).

Seguindo um raciocínio análogo para o campo de senha e o botão de login, criando o teste e depois implementando o layout, podemos obter o resultado desejado na nossa tela.

public class LoginActivityTest
extends ActivityInstrumentationTestCase2 {

public void testLoginActivity_Interface(){
		getActivity();

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

		onView(withId(R.id.lbl_usuario))
			.check(
				matches(
					allOf(
						isDisplayed(),
						withText(R.string.lbl_usuario),							hasSibling(
withId(R.id.txt_usuario)
)
					)
				)
			);

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

		onView(withId(R.id.lbl_senha))
			.check(
				matches(
					allOf(
						isDisplayed(),
						withText(R.string.lbl_senha),
						hasSibling(
withId(R.id.txt_senha)
)
					)
				)
			);

		onView(withId(R.id.btn_login))
			.check(matches(isDisplayed()));
	}
}

Resultando no arquivo de layout da nossa tela de Login, ao corrigirmos nosso código com base nos testes que falham, tornando claro o ciclo red -> green do TDD.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <TextView
	    android:id="@+id/lbl_usuario"
	    android:layout_width="match_parent"
	    android:layout_height="wrap_content"
	    android:text="@string/lbl_usuario" />

	<EditText
	    android:id="@+id/txt_usuario"
	    android:layout_width="match_parent"
	    android:layout_height="wrap_content"
	    android:inputType="text" />

	<TextView
	    android:id="@+id/lbl_senha"
	    android:layout_width="match_parent"
	    android:layout_height="wrap_content"
	    android:text="@string/lbl_senha" />

	<EditText
	    android:id="@+id/txt_senha"
	    android:layout_width="match_parent"
	    android:layout_height="wrap_content"
	    android:inputType="text" />

<Button
	    android:id="@+id/btn_login"
	    android:layout_width="match_parent"
	    android:layout_height="wrap_content" />

</LinearLayout>

Agora, poderíamos especificar, através da criação de um novo ViewMatcher, que o campo senha deve ter um inputType “password”.

Para fechar o ciclo red -> green -> refactor do TDD, poderíamos fazer um ViewMatcher isVisibleAndHasSibling(int siblingId), refatorando o código e agrupando os ViewMatchers isDisplayed e hasSibling.

Montando a Regra de Negócio com TDD

Uma vez pronto nosso layout, podemos partir para a regra de negócio. Começamos com um teste básico: quando tanto o usuário quanto a senha estão vazios, devemos alertar o usuário.

public class LoginActivityTest
extends ActivityInstrumentationTestCase2 {

public void testCamposVazios_DeveApresentarMensagemDeErro(){
		getActivity();

		onView(withId(R.id.txt_usuario)).perform(clearText());
		onView(withId(R.id.txt_senha)).perform(clearText());

		onView(withId(R.id.btn_login)).perform(click());

		onView(withText(R.string.msg_campos_vazios))
			.check(matches(isDisplayed()));
	}
}

Com esse teste, garantimos que os dois campos estão vazios, tentamos realizar o Login e verificamos que uma mensagem é exibida na tela. Rodamos o teste e ele falhou. Voltamos para o código então.

O primeiro passo é implementar a chamada do botão.

public class LoginActivity
extends FragmentActivity
implements OnClickListener {

	private Button btnLogin;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);

		setContentView(R.layout.login_activity);

		btnLogin = (Button) findViewById(R.id.btn_login);
		btnLogin.setOnClickListener(this);
	}

	@Override
	public void onClick(View v) {
		switch(v.getId()){
		case R.id.btn_login:
			AlertDialog dialog =
new AlertDialog(R.string.msg_campos_vazios);
			dialog.show(getFragmentManager(), null);
			break;
		}
	}

}

Observação: A classe AlertDialog é uma classe à parte que estende a classe DialogFragment e apenas exibe uma mensagem passada.

Lembrando que temos que implementar somente o código necessário, não devemos implementar toda a lógica de Login ainda.

Vemos agora que o teste passa, podemos seguir adiante. Para esse exemplo, vamos supor que você tenha uma biblioteca de terceiros ou uma classe que você não conhece ainda, e nem precisa, que é responsável pela validação do Login. Queremos que quando o usuário digite uma combinação válida de usuário e senha, seja exibida uma mensagem de que o Login foi realizado com sucesso.

Como não conhecemos a biblioteca ou quais os usuários válidos, vamos utilizar um mock. Basicamente nós especificamos que quando houver uma chamada ao método de validação dessa biblioteca que não conhecemos com os dados que vamos digitar, ela deve retornar verdadeiro.

Atente para o fato de que precisamos passar esse objeto falso para a Activity através de um método auxiliar.

public class LoginActivityTest
extends ActivityInstrumentationTestCase2 {

public void testLoginValido_DeveApresentarMensagemDeSucesso(){
		String usuario = "User01";
		String senha = "12345";

		loginLibrary = mock(LoginLibrary.class);
		when(loginLibrary.validaUsuario(usuario, senha))
.thenReturn(true);

		getActivity().setLoginLibraryForTests(loginLibrary);

		onView(withId(R.id.txt_usuario))
			.perform(typeText(usuario), closeSoftKeyboard());
		onView(withId(R.id.txt_senha))
			.perform(typeText(senha), closeSoftKeyboard());

		onView(withId(R.id.btn_login)).perform(click());

		onView(withText(R.string.msg_login_sucesso))
			.check(matches(isDisplayed()));
	}
}

Testando a aplicação, o teste anterior continua passando, mas o nosso novo teste falha. Vamos implementar agora o código para que isso funcione.

public void setLoginLibraryForTests(LoginLibrary _loginLibrary){
	loginLibrary = _loginLibrary;
}

@Override
public void onClick(View v) {
	switch(v.getId()){
	case R.id.btn_login:
		String usuario = txtUsuario.getText().toString();
		String senha = txtSenha.getText().toString();

		if(usuario.isEmpty() &amp;&amp; senha.isEmpty()){
			AlertDialog dialog =
new AlertDialog(R.string.msg_campos_vazios);
			dialog.show(getFragmentManager(), null);
		} else {
			if(loginLibrary.validaUsuario(usuario, senha)){
				AlertDialog dialog =
new AlertDialog(R.string.msg_login_sucessos);
				dialog.show(getFragmentManager(), null);
			}
		}
		break;
	}
}

Temos, mais uma vez, nossos testes todos funcionando. Daí em diante, podemos implementar uma série de coisas: dois testes, para que quando qualquer um dos dois campos sozinho esteja vazio, ele também exiba uma mensagem de erro. Depois, acabaríamos por refatorar o código do click no botão, alterando a condição se o usuário e a senha estão vazios por um “ou”.

Em seguida, poderíamos fazer um teste quando a validação ocorre com um problema. Podemos forçar retornos de exceptions com os mocks e uma série de outras coisas.

Conclusão

Com esse tutorial, foi possível ver algumas aplicações do TDD para o desenvolvimento das aplicações, tanto na parte de interface quanto da regra de negócio.

Vimos que pensar na funcionalidade (login, no caso) a partir dos testes, não só é possível, como ajuda a manter o código enxuto, e de fácil manutenção.

E agora, qual é a sua desculpa?

Escrito por Rodrigo Lessinger e Luciano Luzzi

Reuniões de Sprint Divertidas

Você que lidera uma equipe, pare agora e reflita: você tem um grupo de pessoas trabalhando individualmente ou tem efetivamente um time? O seu time gosta de trabalhar com a sua metodologia?

A equipe realmente possui um espírito e um estilo ágil de trabalhar ou apenas usa a metodologia que você implantou?

Durante o mês de abril de 2014, fomos a São Paulo acompanhar o QConSP. No evento, assistimos uma palestra sobre “Retrospectivas divertidas” (Fun Retrospective, por Paulo Caroli e Taina TC Caetano) que me deixou muito curioso e convicto de que poderia adaptar a metodologia ao nosso cotidiano, para trabalhar com alguns dos seus conceitos.

“Fun Retrospective”?

Segundo os autores, a habilidade de aprender e responder rapidamente é uma das características que uma equipe de desenvolvimento possui quando sua situação é bem sucedida. Mas como podemos transformar um grupo de pessoas em uma equipe, que busca aprender com seus erros e compartilhar seus sucessos? Como permitir que ideias opostas tenham igual espaço dentro da discussão? E como conduzir a retrospectiva com passos concretos que levem de fato às melhorias?

Retrospectivas devem ajudar a melhorar a comunicação, produtividade e qualidade no dia a dia. Porém, reunir um grupo de pessoas para falar sobre problemas e discutir melhorias não é tarefa fácil. Por isso, este formato de trabalho tem como objetivo criar um ambiente seguro, em que as pessoas se sintam confortáveis para compartilhar suas reais opiniões.

Conceito

Conforme o livro Fun Retrospective, um time é um grupo de pessoas voltadas para um objetivo comum, mas cada indivíduo deve ajustar suas tarefas, hábitos e as suas preferências de trabalho, para que assim juntos possam atingir um objetivo. A eficácia da equipe depende muito da capacidade que cada integrante tem de trabalhar coletivamente. Ou seja, esta habilidade está diretamente relacionada com a capacidade do grupo em fazer o melhor uso das habilidades dos indivíduos.

Entretanto, um grupo de pessoas não se transforma em uma equipe do dia para a noite. É preciso tempo para criar laços entre colegas, mas este tempo pode ser reduzido com alguma estratégia apropriada. Como eles irão progredir após a sua formação, os membros da equipe podem enfrentar momentos em que eles vão sentir a necessidade de refletir e analisar o passado, ou imaginar e se preparar para o futuro. Esses momentos podem ser recorrentes e, manter o entusiasmo, é a chave para manter a equipe eficaz. Com o tempo, o grupo irá sintonizar a sua capacidade de trabalhar em conjunto. Discordâncias internas deverão ser discutidas, capacidades individuais devem ser levantadas, o time deve buscar continuamente o melhor equilíbrio para a contribuição de todos para um objetivo comum.

Ótimo conceito, mas qual é o objetivo da Fun Retrospective? É fornecer um conjunto de ferramentas de atividades para transformar um grupo de pessoas em uma equipe eficaz. Manter um ambiente descontraído e proporcionar um ambiente onde o time possa refletir, discutir e se divertir, estes são os pontos fundamentais para melhorar continuamente os processos da equipe.

Aplicando alguns conceitos e atividades, dentro do nosso ambiente:

Utilizamos como exemplo, uma das nossas equipes que já trabalha com Scrum. Nela, executamos algumas modificações, a fim de melhorar organização das reuniões e facilitar o envolvimento de participantes na metodologia e no convívio diário.

O primeiro ponto que alteramos foi a forma de organizar e apresentar a pauta da reunião. Com o objetivo de ser o “facilitador” da reunião, passei a usar apresentações em slides mostrando uma contextualização sobre os assuntos que serão trabalhados, para que assim possamos alinhar todos os participantes sobre o que será tratado na reunião.

Exemplo:

  • “Hoje temos como objetivo iniciar um novo projeto para o nosso cliente X.”

  • “Esta retrospectiva é uma recorrente retrospectiva Scrum bi-semanal para a equipe do X projeto.. Estamos no Sprint 15 dos 20 previstos.”

Logo após a contextualização, fazemos uma rápida dinâmica a qual tem o objetivo  de forçar os participantes a refletir sobre sí, sobre o time, processos, satisfação, qualidade entre outros.

Mas qual o sentido de realizar uma atividade dessas?

Simples! Você como líder do time, tem o dever de fazer com que os membros se sintam bem para trabalhar. Então, nosso objetivo é fazer com que os integrantes tenham o projeto e o ambiente de trabalho como o seu “segundo lar”, para que assim possamos evoluir juntos. Sendo assim, a dinâmica entra como três minutos de reflexão, descontração e para você ter um rápido feedback coletivo sobre algum assunto que considera pertinente.

Exemplo de atividade e análise que podemos fazer:

Você solicita que anonimamente os membros informem com um post-it se eles gostam de realizar reuniões às 16 horas de sexta-feira. Após todos terem respondido você nota que todos dizem que não gostam, ou melhor, que odeiam realizar a reunião na sexta. Após receber um feedback como esse, você ainda pretende continuar com a reunião? Espero que não. Nesse momento, será muito inteligente tomar algumas ações como: “pessoal reunião cancelada”. Então você já deve sair da sala pensando em tomar ações para correção desta circunstância. Claro, esse é um exemplo mais simples, mas o feedback pode ser relacionado a temas mais complexos, como pessoas, por exemplo. Aí sim você deve pensar rápido em soluções para contornar ou compreender melhor a dificuldade.

Após o momento da dinâmica, você pode iniciar a sua retrospectiva e dar sequência a reunião, como sempre realiza.

Algumas técnicas que utilizamos:

Check-in

Avaliando a satisfação com as pessoas do time, processos e tecnologias utilizadas.

Com esta dinâmica realizada de forma anônima conseguimos medir a satisfação da equipe com os três pontos.

Retrospective

Com o Hot-Air Ballon é possível fazer o time refletir e discutir sobre aspectos que eles acreditam que seja importante para que o time a cresça e aspectos que fazem com que eles sejam puxados para “baixo”.

Futurespective

Letters to the Future: nesta dinâmica, o time colocou pontos positivos e negativos de si ou do grupo. Após todos realizarem seus apontamentos, conversamos sobre o que foi descrito nos post-its e, ao final, guardamos estes tópicos para que fossem lidos novamente após alguns meses de projeto. Com isso, poderemos analisar se evoluímos e em que aspectos.

É seu papel manter o entusiamo do time

Como já citado, é fundamental mantermos a equipe sempre entusiasmada, mas nem sempre é fácil manter esse sentimento nas pessoas. Muitas vezes, o projeto possui um processo bem desenhado, com regras aplicadas, organização por sprints, etc, mas o time se encontra em uma rotina ou não possui uma “adrenalina” no momento de finalizar um ciclo, seja ele finalizado com sucesso ou não. Então é seu dever fazer com que eles fiquem focados e estimulados a querer ter sucesso em cada sprint.

Após realizar testes básico notamos que o time sofria positivamente com alguns simples gestos. Um simples desafio, um “agrado” pode ser a chave para ajudar no estimulo. No lugar de trabalhar com mais folga, faça com que eles tenham mais dificuldade em fechar o sprint, e caso tudo esteja atrasado os desafie a concluir. Caso eles tenham sucesso, comemore. Não precisa fazer altos investimentos. Refrigerantes e chocolates já podem alegrar a “festa”. Detalhes, podem trazer a felicidade no ambiente de trabalho e a confiança do seu trabalho.

E os resultados?

Sim, tivemos. Este foi um bom formato que encontramos para trabalhar, junto ao time. Concluímos que temos ótimos times, e que hoje somos muito mais auto gerenciáveis. A cada sprint a equipe busca se motivar mais e mais. Com as dinâmicas, conseguimos compreender melhor cada integrante do grupo e encontrar caminhos para que eles possam ser mais efetivos e satisfeitos dentro do projeto.

Claro a metodologia não acaba aqui e também não é uma receita de bolo. Para que a metodologia tenha sucesso, uma série de outros processos são necessários. Este é apenas um dos rituais utilizados para seguirmos um caminho melhor dentro dos nossos projetos.

 Escrito por Marcel Guinther

ALM Parte 2 – Dicas sobre o uso do TFS como source control

Hoje continuaremos a série de posts relacionados ao ciclo de vida de uma aplicação. No artigo anterior sobre esse tema, falamos um pouco sobre a importância que os conceitos de ALM têm nos nossos produtos, e introduzimos brevemente algumas ferramentas que a Microsoft nos fornece para cada estágio do processo.

Discutiremos agora, tópicos mais técnicos e específicos sobre o ponto central de toda solução de ALM da Microsoft, o Team Foundation Server (TFS). Serão apresentadas algumas features relacionadas ao controle de versão, dicas de ferramentas auxiliares e práticas que ajudam o time a evitar problemas futuros e melhorar a sua produtividade.

Esse post assume que a instalação do TFS já foi efetuada, e que os serviços necessários já estão em operação. O TFS é um sistema complexo, que contém diversas partes além do source control em si, e portanto a sua instalação requer bastante atenção aos requisitos mínimos e às etapas, como mostra esse tutorial sobre a instalação do componente.

Conectando o Visual Studio ao TFS

Com a etapa de instalação fora do caminho, o primeiro passo para trabalharmos em cima do TFS é conectá-lo ao Visual Studio. Esse é um passo simples, que deve ser feito apenas uma vez ou em caso de alterações nos endereços do TFS. No Team Explorer, clique no ícone com formato de um plugue, “Connect to Team Projects”:

Uma interface será exibida para seleção de servidores TFS e projetos:

Nessa imagem, retirada de uma máquina local, podemos ver que existem três servidores configurados. O primeiro, com a url https://julealgon.visualstudio.com, é a minha conta pessoal no Visual Studio online, uma versão gratuita do TFS (para até 5 desenvolvedores) disponibilizada via cloud services no Azure. Os demais são os links para os nossos dois servidores internos que provêm versões distintas do TFS, já que alguns projetos antigos ainda não foram migrados para o TFS 2013.

Ao conectar com um servidor recém-instalado, apenas a coleção padrão será apresentada, visto que ainda não criamos nenhum projeto. Mais informações sobre o conceito de “coleção de projetos” e quais as vantagens e desvantagens de separar os projetos em mais de uma coleção podem ser vistos aqui.

Criando um projeto

A criação do projeto se encontra na mesma parte do Team Explorer que utilizamos para conectar o TFS ao Visual Studio:

Na criação do projeto, é necessário um nome e uma descrição. A descrição é apenas visual e é utilizada posteriormente nas páginas web de controle do TFS, podendo ser editada livremente. Já o nome do projeto é extremamente importante e deve ser decidido com cuidado, pois não pode ser alterado futuramente.

O próximo passo é também extremamente importante e deve ser estudado com cautela antes da criação do projeto: a escolha do template:

Os templates exibidos acima dependem basicamente da versão do TFS, portanto, as opções em uma instalação limpa (ou mais antiga) podem ser diferentes. De qualquer forma, os templates padrão são variações dos 3 principais: Scrum, Agile e CMMI. Nós nos focaremos em alguns detalhes que distinguem o Agile do Scrum, e não comentaremos especificamente sobre o CMMI, destinado a processos mais formais e rígidos de desenvolvimento, que necessitam de mais rastreamento. Para ver todas as diferenças entre os 3 tipos de templates, esse artigo na MSDN é um ótimo começo.

A escolha do template é importante pois interfere diretamente nos workitems que podem ser associados ao projeto. Um workitem, por sua vez, é uma unidade de rastreamento no ciclo de vida do projeto. Um exemplo clássico de um workitem é o Bug, que representa um defeito em algum ponto do software ou algo que está fora da especificação. Outro workitem básico é a Task, que define uma tarefa auto contida a ser executada por um desenvolvedor. As diferenças no tratamento desses items de desenvolvimento devem guiar a escolha do template, de forma que o workflow fornecido pelo TFS, se assemelhe ao máximo ao processo adotado pelo time.

Na Mobiltec, alguns projetos utilizam o Agile e outros o Scrum. A escolha deve partir do gerente do projeto ou da pessoa encarregada de agendar as próximas atividades relacionadas ao projeto, como o Product Owner no caso do Scrum. Algumas das diferenças principais entre os templates Agile e Scrum são as seguintes:

- O template Agile é bastante genérico e segue os princípios do desenvolvimento ágil, com sprints, user stories, etc. Já o Scrum é um template mais enxuto, e utiliza terminologias específicas do método Scrum, como Product Backlog Items (PBI). A terminologia é bastante relevante na adoção de um template se o time tiver um histórico com um método particular antes da utilização do TFS.

- O tratamento de previsão de tempo de desenvolvimento, medido em “tempo restante” no Scrum, também é mais simples que o utilizado no Agile, onde as estimativas do tempo inicial são preservadas nos workitems.

- O tratamento de bugs é diferente entre os dois processos. No Scrum, bugs são items de desenvolvimento considerados da mesma forma que os PBIs, ou seja, são tratados como entidades de primeira ordem e são utilizados no processo de backlog planning para fechamento dos sprints. Por outro lado, o Agile trata os bugs da mesma forma que as tasks, entidades de segunda ordem, não os exibindo no backlog e os considerando de certa forma como débito técnico.

Com a escolha do template em mãos, a próxima etapa é a decisão entre utilizar o TFS ou o Git como controle de versão:

A possibilidade de integração com o Git é relativamente recente e não será abordada nesse post, portanto escolheremos a opção “Team Foundation Version Control”.

Isto é tudo o que precisamos para criar o nosso primeiro projeto.

Estrutura do projeto:

Após a criação do projeto no TFS, a primeira decisão importante a ser tomada é como estruturar as pastas com os diversos módulos e dependências. A estrutura interna do projeto no TFS é absolutamente livre, e pode seguir qualquer padrão que faça sentido para o time de desenvolvimento. No entanto, existem alguns conceitos que são mais facilmente aplicados em pastas do que em arquivos diretamente. Um desses mecanismos é o “branch”. Por essa razão, mesmo que não exista uma necessidade imediata de criar branches, é uma boa prática criar uma pasta principal para o desenvolvimento e não apenas colocar os módulos na raiz do TFS. Assim, é possível posteriormente criar um branch do projeto, com o mínimo de esforço. Portanto, a primeira dica sobre a estrutura é ter uma pasta raiz. Os nomes mais utilizados para tal pasta são “Main” e “Trunk”

Com a pasta raiz definida, já é possível desenvolver projetos simples não versionados, com times pequenos, sem nenhuma outra alteração. Vejamos a estrutura mais simples, com uma pasta central para o projeto, utilizada pelo Mobiltec.Framework, um projeto de funcionalidades genéricas utilizadas pelos demais projetos da empresa. As pastas “Build” e “TeamProjectConfig” são geradas por outros componentes e podem ser ignoradas por enquanto:

No entanto, é bastante corriqueiro que versões paralelas do projeto precisem ser mantidas simultaneamente. Um caso simples dessa necessidade é existir uma pasta que representa a versão atual mais estável, e uma outra pasta, onde o desenvolvimento corrente está sendo feito. Nesse modelo, quando as alterações são testadas e julgadas estáveis, elas são transferidas para a linha estável, dando início a um novo ciclo de desenvolvimento. Esse modelo simplificado é atualmente utilizado no projeto cloud4mobile, que está em fase beta:

Essas estruturas de pastas e a sincronização delas são gerenciadas pelo mecanismo de branching e merging do TFS. Ao processo de decisão da estrutura, é de costume atribuir o nome de “branching strategy”.

Um branch é um vínculo entre um elemento (arquivo ou pasta) e uma cópia dele em um outro ponto da estrutura. No exemplo anterior com duas linhas, existe uma pasta Main, que representa o projeto estável, e uma segunda pasta Dev, onde o desenvolvimento do sprint corrente é feito. Nesse caso, Dev é um branch da Main. Quando existe uma relação de branch entre pastas no TFS, o ícone exibido é diferente do de uma pasta comum, como pode ser visto na imagem.

Em uma estrutura mais complexa, é possível ter diversos níveis na hierarquia, e também é factível que existam diversos branches da mesma pasta ou arquivo, em pontos distintos. Vejamos a estrutura utilizada no projeto de integração de um cliente da solução de MDM M3:

Essa estrutura é ligeiramente mais complexa por algumas razões:

  1. Ao contrário do cloud4mobile, diversas releases devem ser mantidas em paralelo, cada uma com possíveis correções de bugs etc;
  2. Em “Development”, diversos branches podem estar ativos concorrentemente. Por algum tempo, a prática de feature branching foi comum no projeto, e diversos times poderiam ser alocados para trabalhar em features distintas, cada uma com os seus prazos;
  3. Existe um nível a mais na hierarquia de branches: o branch “Release 2 Patch8” é um branch do “Release 2”, e o “Release 2” é um branch da main.

É comum categorizar essas estruturas por nível de complexidade, e esse documento mostra e discute todas as estratégias mais comuns, incluindo as que acabamos de mostrar.

Dicas sobre branching, merging e resolução de conflitos:

Apenas a teoria de estratégias de branch não é suficiente para manter um produto evitando problemas e dificuldades no controle de versão. Esse artigo mostra diversas boas práticas em relação ao source control como um todo, no entanto, nos limitaremos nesse parágrafo em citar alguns pontos que já causaram muita dor de cabeça para diversos times da Mobiltec:

  • Sempre tenha a última versão do código antes de efetuar qualquer check-in:

Ao efetuar a sincronização do repositório local (workspace) com o servidor, é possível que conflitos de alteração sejam detectados e tenham que ser corrigidos. Em outras situações é possível que conflitos sejam detectados e corrigidos automaticamente pelo Visual Studio. Em ambos os casos, problemas podem ocorrer ao executar o código sincronizado, portanto é importante que todas as modificações sejam retestadas contra a última versão.

  • Faça merges entre os branches o mais frequentemente possível:

Em estratégias de branch simples, é fácil controlar todas as mudanças de forma que o código esteja sempre atualizado. No entanto, em estruturas complexas, muitas vezes um branch começa a ficar defasado em relação aos demais. Por exemplo, nos branches de release citados anteriormente, é importantíssimo que as correções de bugs efetuadas diretamente nas releases sejam integradas com a linha principal e com os branches de desenvolvimento o mais cedo possível. Isso evita que a operação de merge se torne mais complexa do que deveria (muitas alterações a serem integradas) e também evita que um mesmo problema seja “detectado” em branches distintos e, o que é pior, corrigidos em pontos distintos utilizando lógicas diferentes. Sincronizar os branches frequentemente também facilita a integração de novas funcionalidades com o mínimo de conflitos possível, evitando bugs por resoluções incorretas.

  • Evite ao máximo a criação de um branch:

A gerência dos branches se torna extremamente complexa quanto mais branches são criados. Se houver apenas a suspeita de que um branch pode ser útil no futuro, mas atualmente não existe a necessidade, deixe para criar o branch quando ele precisar ser utilizado. Um branch pode ser criado a partir de um changeset específico ou de um label, que pode ser criado a qualquer momento por questões de organização.

  • Evite check-ins com múltiplas alterações não correlacionadas:

Fazer alterações focadas, com um propósito único, ajuda enormemente no momento da resolução de conflitos, nas operações de merge e na detecção de problemas via análise do histórico de alterações. Evite por exemplo resolver um bug e ao mesmo tempo refatorar a classe inteira: faça as duas operações em check-ins distintos. O mesmo vale para implementar uma nova funcionalidade e corrigir um bug sem relação.

  • Evite o merge de arquivos ou changesets isolados e prefira o merge de todo o branch:

Fazer o merge do branch inteiro geralmente evita conflitos futuros com changesets que não foram sincronizados e ficaram “perdidos”.

  • Prefira nomes curtos para pastas principais, como “Dev” em vez de “Development”:

Essa geralmente não é uma preocupação em times que desenvolvem apenas em .Net, mas pode ser uma grande dor de cabeça se projetos Android forem também controlados pelo TFS por exemplo. Por uma limitação do .Net, o TFS não suporta caminhos de arquivos com mais do que 260 caracteres, o que facilmente pode ocorrer nos projetos java devido a criação de diversas subpastas para o mapeamento dos namespaces e dos arquivos de output.

Check in Policies:

No TFS existe uma feature interessante, conhecida por “políticas de check in”. Essas políticas devem ser configuradas para todo o projeto, e servem como uma validação antes de efetuar o check in nas máquinas de desenvolvimento. Elas podem variar em funcionalidade, mas a ideia é sempre a de garantir uma melhor qualidade de código o mais cedo possível. As políticas instaladas por padrão podem ser consultadas através do Team Explorer, na aba Settings>SourceControl:

Nessa janela são exibidas todas as políticas e uma breve descrição da sua aplicação. Cada projeto tem as suas particularidades, portanto o conjunto de políticas configurado para cada um tende a ser diferente. No cloud4mobile, atualmente apenas a política de execução de Code Analysis é aplicada, juntamente com o “Custom Path Policy”, que restringe a aplicação de uma outra política baseado no caminho dos arquivos no source control. No projeto do M3, algumas outras políticas foram utilizadas, como a que obriga uma descrição para cada changeset ou a que bloqueia os check-ins caso o último check-in tenha causado uma falha na compilação.

É possível também criar políticas customizadas, mas o processo não é tão simples, pois requer alteração no registro do windows para o cadastro das políticas nas máquinas cliente. Algumas ferramentas como o Team Foundation Server Power Tools instalam também políticas de check-in, como é o caso da “Custom Path Policy” citada anteriormente. Outras políticas são desenvolvidas como pacotes e disponibilizadas via nuget ou downloads, como esse pacote de políticas ou a política que obriga a execução do StypeCop nos arquivos. A execução de políticas de check-in é extremamente benéfica pois evita que o código salvo no TFS pare de compilar por problemas simples, e também retira o overhead de um servidor de build (assunto para um próximo post) tem em executar um build desnecessariamente.

Configuração de soluções:

Outro ponto importante sobre a estrutura dos projetos é a sua representação virtual, conhecida no Visual Studio por “solutions”. Uma solution é uma coleção de projetos que representa um determinado produto. Assim como vimos nas estratégias de branching, existem diversas maneiras de organizar os projetos em solutions. Novamente, o MSDN é uma ótima referência sobre esse assunto, e detalha as três principais estratégias, que são:

  • Master Solution: o projeto é contido em uma única solução, incluindo todas as dependências. Esse é atualmente o método utilizado na maioria dos projetos da Mobiltec, e é fortemente recomendado sempre que possível, visto que manter diversas solutions é consideravelmente mais trabalhoso quando há necessidade de alteração nos projetos. Segue um exemplo dessa forma, utilizada no cloud4mobile:
  • Múltiplas Solutions: Os projetos são categorizados de alguma forma e separados em solutions completamente distintas. Essa estratégia é utilizada por nós principalmente no Mobiltec.Framework, onde existe a necessidade de atender a diversas versões do .Net framework. Nesse caso, para cada plataforma que se deseja atender, uma solution diferente é criada, com projetos específicos para aquela versão. O Mobiltec.Framework atualmente é compilado para .Net Compact Framework 3.5, .Net 4.0, .Net 4.5, Silverlight 5, e Android. A compilação android obviamente não utiliza o conceito de solution, mas para cada uma das demais plataformas existe uma solution distinta. Nesse tipo de separação, recomendamos que as solutions estejam na mesma pasta no source control, para simplificar a configuração da integração com o nuget e permitir o compartilhamento dos pacotes. A figura a seguir mostra a estrutura utilizada no Mobiltec.Framework:

 

  • Sub Solutions + Master Solution: Existe uma master solution com todos os projetos, mas além dela outras solutions menores, que funcionam como views distintas do produto como um todo, são criadas e mantidas separadamente. Na nossa experiência, essa estratégia é bastante prejudicial devido ao alto overhead de manutenção: quando um módulo é alterado, diversas solutions precisam ser atualizadas. Esse modo foi utilizado por um bom tempo no projeto M3, mas nos últimos tempos tem sido abandonado em favor de uma master solution. Infelizmente é a única forma de tratar a complexidade e o tempo de compilação e carregamento das solutions se a quantidade de projetos for grande demais (100+ projetos).

 

Ainda no contexto de manipulação de projetos e solutions, uma outra dica do nosso time é, independentemente da estratégia de partição escolhida, utilizar sempre que possível referencias de projeto, e não referencias de arquivo. Quando uma referência de projeto é estabelecida, os projetos não ficam fortemente acoplados a caminhos fixos, e o MSBuild (ferramenta utilizada na compilação dos projetos .Net) utiliza essa informação de link entre os projetos para reconhecer alterações e compilar apenas o que for necessário. Além disso, ao alternar a configuração de build, por exemplo entre Debug e Release (que são as configurações padrão), os caminhos de saída de cada projeto são considerados e as referências corretas são utilizadas, ao contrário do que acontece com as referências de arquivo, que contêm um caminho fixo.

 

No passado, o próprio M3 utilizava referências por arquivo: cada projeto, quando compilado, copiava o seu output para uma pasta fixa conhecida, de onde todos os outros projetos referenciavam suas dependências. Esse processo era extremamente lento, conflitava com a estrutura de pastas dos branches por não utilizar um caminho relativo aos fontes, e acabava forçando a configuração manual de ordem de compilação em diversos projetos, novamente caindo no velho problema de manutenção. Quando a troca foi feita para referências por projeto, toda essa infraestrutura foi abandonada e o processo se tornou muito mais simples e robusto: as configurações de ordem antes manuais agora são gerenciadas pela árvore de dependência gerada pelo MSBuild, garantindo que todas as dependências sejam compiladas para cada projeto, apenas uma única vez.

Conclusão:

Como dito no início, o TFS é uma ferramenta complexa, composta de diversas partes. Não foi a intenção desse post cobrir exaustivamente todas as features do componente de source control (até porque isso equivaleria a centenas de páginas) mas sim, dar uma visão geral sobre os pontos mais básicos e estruturais, focando em problemas que já foram enfrentados pelos nossos times de desenvolvimento.

 

Em um próximo momento, falaremos um pouco sobre o próximo componente da cadeia de ALM, o TFS Build, responsável por efetuar a compilação automática do código para fins de validação.

Escrito por Juliano Gonçalves

A culpa é do sistema: decisões mal planejadas podem arruinar muito mais do que dados

Quem nunca ouviu isso ao entrar em contato com um prestador de serviços indignado porque uma operação crítica não está funcionando? Desde gestores técnicos de grandes corporações até o usuário final, todos sofrem as mazelas de uma implantação ou migração mal sucedida.

A bola da vez é a Dafiti, uma das maiores e mais bem sucedidas empresas de e-commerce. A loja, que com muito esforço construiu uma reputação sólida e ganhou credibilidade pela qualidade dos seus produtos, processos e atendimento, está agora em maus lençóis. Segundo matérias publicadas pela imprensa, desde junho a empresa sofre de um “apagão” no atendimento. Serviços como entregas, trocas ou devoluções estão inativos e os insistentes contatos dos consumidores lesados estão sendo ignorados, por determinação da direção da empresa.

Segundo matéria publicada no Estadão, a polêmica decisão foi porque “não há o que dizer aos clientes agora. Quando houver algo a ser dito, eles serão comunicados.” Com isso, somente no mês de julho, o site de reclamações Reclame Aqui registrou um número que se aproxima das 11 mil reclamações contra a empresa. Além disso, diversos processos foram abertos e o Procon de SP gerou uma multa superior a R$ 300 mil. Isso sem considerar os danos à imagem, que dificilmente terá sua credibilidade recuperada. Talvez no longo prazo.

A culpa é do sistema

Em nota oficial, a Dafiti informa que está realizando uma migração de tecnologia, implantando um sistema que custou à empresa R$ 20 milhões. Antes mesmo da virada de chave, a comerciante iniciou ações com o objetivo de alertar os consumidores de que as comunicações e prazos estariam prejudicados, até que tudo estivesse funcionando. O problema, é que o consumidor não quer saber se a culpa é do sistema, do hardware, do atendente, diretor ou de quem quer que seja. E em geral, o cliente vai ser o maior afetado por qualquer falha tecnológica que venha a ocorrer. Logo, nesse caso, a empresa poderia somar ao investimento o alto custo dos danos à sua imagem, as multas, processos judiciais e as vendas perdidas, o que facilmente poderá triplicar os custos de implantação.

O case acima expôs os riscos de uma decisão mal planejada e uma ação mal executada. Será que antes de aprovar a aquisição dessa tecnologia, todos esses transtornos nessa escala haviam sido previstos? Dificilmente.

Foco nos objetivos da organização

Quando uma empresa toma a decisão de utilizar determinada tecnologia, ela deve analisar todos os possíveis efeitos que aquela decisão causará. E quando delegar a atividade para seus times e fornecedores, é importante compartilhar as implicações para que todos tenham ciência das responsabilidades que essa ação gera. Expondo ao time a importância e riscos da decisão, talvez, aquele analista mais quietinho que tenha previsto um possível atraso ou falhas na virada de chave, seja mais cauteloso ao expor suas percepções técnicas.

Plano B

Não sabemos de fato o que aconteceu no caso da Dafiti. Não estamos lá dentro e nem temos negócios com essa empresa. Mas utilizemos o case como alerta, pois em um caso como esse, onde a mudança no sistema vai afetar drasticamente as operações, o ideal é que a migração seja feita de forma gradual e que haja um plano de contingência que permita fazer um downgrade, caso a virada de chave mostre falhas.

Que fique o exemplo para todos, sejam fornecedores de TI, como é o caso da Mobiltec, seja para as grandes corporações. É importante nunca esquecer que os sistemas todos servem apenas para melhorar processos que aumentem a produtividade e consequentemente lucratividade das empresas. Se isso não for uma premissa, então a decisão já nasce comprometida.

 Escrito por Aline Roque

Desvendando as DataInteractions do Espresso

Não é a primeira vez que falamos sobre Espresso aqui no blog. Para quem não leu, no primeiro post sobre os testes de interface utilizando o Espresso, foram mostrados os passos iniciais para quem quer utilizar a API da Google em seus testes. Agora, vamos abordar alguns conceitos mais práticos com relação às DataInteractions, conhecer o código por trás da aplicação, o que torna muitas vezes mais fácil entender como utilizá-las.

Para melhor acompanhar o post, recomendo que você já tenha clonado o repositório do Espresso (https://code.google.com/p/android-test-kit/) ou ao menos baixado o código fonte (https://code.google.com/p/android-test-kit/source/browse/).

Todos os códigos aqui mostrados são referentes à versão 1.1 (última na data do post).

Ao iniciar a implementação dos testes de interface, creio que uma das maiores dificuldades na utilização do Espresso seja lidar com Listas, Spinners ou qualquer outra classe derivada de AdapterViews. Esse tipo de View não carrega todas as informações quando o usuário chega à tela, de modo que só existem efetivamente uma parte das Views na tela, não sendo tão simples acessar os elementos para executar ações e verificações.

Como havia citado no primeiro post, o mais importante é se atentar ao fato de que deve-se procurar um objeto em específico, não a View que contém os dados desse objeto. Mas por quê? Como funciona isso?

Bom, primeiramente, vamos dar uma olhada nos métodos que a classe DataInteraction possui:

public DataInteraction onChildView(Matcher childMatcher)
public DataInteraction inRoot(Matcher rootMatcher)
public DataInteraction inAdapterView(Matcher adapterMatcher)
public DataInteraction atPosition(Integer atPosition)
public DataInteraction usingAdapterViewProtocol(AdapterViewProtocol adapterViewProtocol)

public ViewInteraction perform(ViewAction... actions)
public ViewInteraction check(ViewAssertion assertion)

private AdapterDataLoaderAction load()
private Matcher makeTargetMatcher(AdapterDataLoaderAction adapterDataLoaderAction)
private Matcher displayingData(final Matcher adapterMatcher,
final Matcher

 

O primeiro grupo é utilizado para especificar os parâmetros (discutidos no final do post) para buscar nossa DataInteraction, o último são métodos privados utilizados internamente pela classe.

Os dois métodos que mais interessam para entender como funciona são os do segundo grupo (spoiler: na verdade eles chamam os métodos do terceiro grupo, mas vamos por partes), que é quando o Espresso efetivamente busca a View que você procura:

public ViewInteraction perform(ViewAction... actions) {
    AdapterDataLoaderAction adapterDataLoaderAction = load();

    return onView(makeTargetMatcher(adapterDataLoaderAction))
                 .inRoot(rootMatcher)
                 .perform(actions);
}

public ViewInteraction check(ViewAssertion assertion) {
    AdapterDataLoaderAction adapterDataLoaderAction = load();

    return onView(makeTargetMatcher(adapterDataLoaderAction))
                 .inRoot(rootMatcher)
                 .check(assertion);
}

(Sim, em suma, eles fazem a mesma coisa para buscar os dados.)

Primeiramente, eles utilizam uma ViewAction para encontrar e carregar o dado procurado no AdapterView:

private AdapterDataLoaderAction load() {
    AdapterDataLoaderAction adapterDataLoaderAction =
        new AdapterDataLoaderAction(dataMatcher, atPosition, adapterViewProtocol);

    onView(adapterMatcher)
          .inRoot(rootMatcher)
          .perform(adapterDataLoaderAction);

    return adapterDataLoaderAction;
}

Sim, vamos ter que ir mais fundo no código (codeception) para entender, mas vamos direto ao método perform dessa ViewAction, que é onde a mágica acontece:

AdapterView adapterView = (AdapterView) view;
List matchedDataItems = Lists.newArrayList();

for (AdapterViewProtocol.AdaptedData data : adapterViewProtocol.getDataInAdapterView(adapterView)) {
    if (dataToLoadMatcher.matches(data.data)) {
        matchedDataItems.add(data);
    }
}

Nessa primeira parte, basicamente, ele utiliza um AdapterViewProtocol para buscar todos os dados do AdapterView.

A classe AdapterViewProtocol em geral não é algo a se preocupar. Ela apenas abstrai algumas questões de scroll e busca dos itens. A não ser que você implemente uma classe derivada da AdapterView que tenha propriedades muito diferentes da original, em geral, o protocolo padrão que ele utiliza não trará problemas.

O método getDataInAdapterView utiliza o método getItemAtPosition(int position) da classe AdapterView, iterando sobre todas as posições. Então, é com o retorno desse método que você deve se preocupar na hora de buscar um item. Se você analisar a sua classe do adaptador será fácil entender como buscar o item que você quer utilizar.

O dataToLoadMatcher é o matcher que você passou para o parâmetro onData. Então, por exemplo, se você tem uma ListView com um determinado Adapter que tem dois tipos de itens, ele vai iterar sobre todos os itens, mas se você tiver especificado uma classe (is(instanceOf(classeX.class))), a lista matchedDataItems vai conter somente os objetos daquela classe.

Após isso, ele verifica se ao menos um item bate com o matcher passado, e, caso sim, segue adiante:

if (atPosition.isPresent()) {
    int matchedDataItemsSize = matchedDataItems.size() - 1;
    if (atPosition.get() &gt; matchedDataItemsSize) {
        throw new PerformException.Builder()
                                  .withActionDescription(this.getDescription())
                                  .withViewDescription(HumanReadables.describe(view))
                                  .withCause(new RuntimeException(String.format(
                                             "There are only %d elements that matched but requested %d element.",
                                             matchedDataItemsSize, atPosition.get())))
                                  .build();
    } else {
        adaptedData = matchedDataItems.get(atPosition.get());
    }
} else {
    if (matchedDataItems.size() != 1) {
        StringDescription dataMatcherDescription = new StringDescription();
        dataToLoadMatcher.describeTo(dataMatcherDescription);
        throw new PerformException.Builder()
                                  .withActionDescription(this.getDescription())
                                  .withViewDescription(HumanReadables.describe(view))
                                  .withCause(new RuntimeException("Multiple data elements " +
                                           "matched: " + dataMatcherDescription + ". Elements: " + matchedDataItems))
                                  .build();
    } else {
        adaptedData = matchedDataItems.get(0);
    }
}

Basicamente, se você especificou a posição do elemento desejado, ele busca entre os itens que bateram com o matcher passado, o item que está naquela posição. Se não, você deve passar um matcher que coincida com um e exclusivamente um elemento, que será retornado.

Por fim são realizadas algumas operações na Thread principal (onde ocorrem as operações de UI) tentando realizar um scroll até o item requisitado.

Uma vez encontrado o item e realizado o scroll com sucesso, o método makeTargetMatcher é o responsável por, de certa forma, transformar o nosso dataMatcher em um viewMatcher:

private Matcher makeTargetMatcher(AdapterDataLoaderAction adapterDataLoaderAction) {
    Matcher targetView = displayingData(adapterMatcher, dataMatcher, adapterViewProtocol,
                                              adapterDataLoaderAction);
    if (childViewMatcher.isPresent()) {
        targetView = allOf(childViewMatcher.get(), isDescendantOfA(targetView));
    }

    return targetView;
}

E o método displayingData retorna um ViewMatcher com as seguintes características:

Optional data = adapterViewProtocol.getDataRenderedByView( (AdapterView) parent, view);
if (data.isPresent()) {
    return adapterDataLoaderAction.getAdaptedData().opaqueToken.equals(data.get().opaqueToken);
}

O que está acontecendo aqui? Bom, ele busca entre todas as Views da tela uma que seja renderizada pelo AdapterView em questão, ou seja, que esteja dentro do AdapterView, e que esteja na mesma posição do item encontrado com o DataMatcher.

Então, por fim, depois de carregar o dado procurado e realizado o scroll até ele, podemos utilizar esse ViewMatcher como parâmetro do método onView, como é feito nos métodos check e perform da classe DataInteraction, já que agora temos certeza de que a View está na tela.

 

E sobre os parâmetros? Bom, a partir do código acima e dando uma olhada nas inicializações feitas na classe DataInteraction, podemos chegar às seguintes conclusões:

public DataInteraction onChildView(Matcher childMatcher)

  • Não é estritamente necessário para localizar um item dentro do AdapterView.
  • Valor Padrão: Não possui (Se não for especificado, não é utilizado)
  • Utilização: Quando quiser se referir a alguma view específica dentro do layout do item em questão
  • Exemplo: Se utilizar Layout personalizado para os itens de uma ListView que tenha duas labels e queira se referir somente a uma delas e não ao Layout base para fazer uma verificação ou executar uma ação.

public DataInteraction inRoot(Matcher rootMatcher)

  • Valor Padrão: Janela que estiver com o foco.
  • Utilização: Quando quiser se referir a algum AdapterView que não esteja na janela exibida atualmente.
  • Exemplo: Se uma janela de diálogo estiver sendo exibida e você quer se referir a um AdapterView da Activity que está por trás da mensagem de diálogo.

Cuidado ao utilizar esse método! O AdapterView deve existir na tela ainda. Você não pode utilizar isso para se referir a um layout de uma Activity que não é mais mostrada na tela.

public DataInteraction inAdapterView(Matcher adapterMatcher)

  • Valor Padrão: isAssignableFrom(AdapterView.class)
  • Utilização: Sempre que houver mais de um AdapterView.
  • Exemplo: Uma activity com uma ListView e um Spinner, poderia-se utilizar, para se referir ao Spinner:
    inAdapterView(isAssignableFrom(Spinner.class))

public DataInteraction atPosition(Integer atPosition)

  • Valor Padrão: Não possui (Se não for especificado, utiliza o primeiro e único elemento retornado)
  • Utilização: Sempre que o matcher especificado bater com mais de um item dentro do AdapterView.
  • Exemplo: Clicar no primeiro item do tipo String dentro de um AdapterView:
    onData(is(instanceOf(String.class))).atPosition(0)

public DataInteraction usingAdapterViewProtocol(AdapterViewProtocol adapterViewProtocol)

  • Valor Padrão: Protocolo padrão implementado pelo Espresso (Classe StandardAdapterViewProtocol)
  • Utilização: Implementações de AdapterViews que apresentam problemas com a implementação padrão.
  • Exemplo: ExpandableListView é um exemplo citado nos comentários do código da classe AdapterViewProtocol em que não se pode usar os métodos getAdapter e getItemAtPosition, de modo que é necessário implementar uma interface a parte para utilizar o método onData.
Esperamos que os conceitos apresentados possam ser úteis para quem deseja se aventurar na utilização das DataInteractions da API do Google.
Escrito por Rodrigo Lessinger