quarta-feira, 7 de setembro de 2011

List ou IList: eis a questão

Este texto é uma tradução do post que escrevi para o blog Dive Into Code.


Em um dia de reflexão durante uma conversa no trabalho, surge uma questão: Por que tantos desenvolvedores utilizam sempre referência para List<T> no lugar de IList<T> em C#? Mas a resposta convincente para esta pergunta não apareceu. Dessa forma o princípio de depender de abstrações e não de implementações concretas estaria sendo desprezado.


Aparentemente não é a primeira vez que esta dúvida aparece. E é possível notar que boa parte dos desenvolvedores que usam List<T> no lugar de IList<T> já foram programadores Java um dia. Seria este o motivo?


Em C# costuma-se utilizar a letra “I” no início do nome de interfaces. Pessoas que já programaram em Java poderiam se confundir, uma vez que List em Java representa a interface e LinkedList e Stack seriam exemplos de implementações. E é importante notar que, em C#, LiskedList<T>, Queue<T> e Stack<T> não implementam IList<T>, pois não fornecem acesso por índice.


Torna-se necessário entender mais a classe genérica List<T>, e as interfaces que ela implementa (IEnumerable<T>, ICollection<T>, IList<T>, IEnumerable, ICollection e IList). Assim, tais interfaces poderiam ser utilizadas como referência no lugar da classe concreta.


Uma vez que foi entendido um pouco mais sobre as interfaces e o que elas forneciam em termos de funcionalidade, era necessário fazer as alterações em algum projeto que fosse utilizado sempre List<T>. No primeiro momento, substituindo ao menos o retorno de um método de List<T> para IEnumerable<T>. Com esta mudança, apareceram alguns erros de compilação, faltava o método Add(T). Porém com ICollection<T> o código funcionou normalmente. Em alguns outros casos o IEnumerable<T> atendia a necessidade, pois só era necessário percorrer os ítens da lista retornada.


Mais adiante, foi encontrada uma situação em que era necessário acessar um ítem específico da estrutura, que foi resolvido utilizando a interface IList<T> como retorno. Até que apareceu um AddRange(IEnumerable<T>) de List<T> sendo utilizado, que poderia ser facilmente substituído por um extension method para ICollection.


Mas depois de todas as alterações surge a pergunta número dois: Todas as operações estavam sendo realizadas no local correto? Ficava mais fácil notar que determinadas classes estavam fazendo mais do que deveriam fazer, uma vez que o retorno de cada método era mais consciente.


No final, a refatoração obteve sucesso, todos os retornos de métodos que eram List<T> foram substituídos por uma interface adequada. Com um pouco mais de pesquisa foi possível notar que não só desenvolvedores com costume da interface List em Java faziam referência para List<T>, mas muitas pessoas utilizam a referência para List<T> devido aos recursos providos pela implementação. Um método como AddRange(IEnumerable<T>) poderia ser chamado em qualquer local. Uma proposta tentadora. Entretanto, depender de abstrações faz com que o código se torne mais flexível. E depender de interfaces que contem apenas as funcionalidades necessárias, limita a não tentar fazer em uma classe o que não deveria ser responsabilidade dela.

terça-feira, 26 de abril de 2011

Caixa de papel, vidro, webcam e voilà! Um piano multitoque

Na faculdade, mais especificamente na cadeira de Processamento Digital de Imagem e Voz, era necessário a elaboração de um projeto que pudesse utilizar de conceitos demonstrados nas aulas. Surgiu, então, a idéia de Marcus Lira para criar um piano multitoque.

Inicialmente a idéia parecia complicada de ser alcançada em apenas um período. Alguns integrantes do grupo estariam concentrando forças no trabalho de conclusão de curso e todos tinham outras cadeiras a cumprir. Mas seguimos com esse projeto.

O que estávamos nos propondo a fazer não se tratava de algo novo e tinhamos direcionamento para seguir. Utilizamos, então, o CCV para fazer boa parte do nosso trabalho. Como iríamos usar o protocolo TUIO, procuramos uma implementação de cliente que pudesse atender nossas necessidades. E assim, selecionamos o MT4j para desenvolver a UI.

Para a reprodução do som do piano escolhemos utilizar o Minim 2.1.0 BETA.

O código da aplicação desenvolvida pode ser encontrado no repositório do Google code. E como resultado segue o primeiro vídeo. Não sou bom em tocar piano, então não tente encontrar uma melodia.



Como as coisas estavam meio bagunçadas nesse primeiro vídeo, foi feita uma nova versão. Nessa segunda versão do device, houve melhora na captação do toque devido a iluminação que entrava na caixa.


Mais informações, sobre como o projeto foi realizado, podem ser encontradas no relatório que escrevemos. E sim, o trabalho foi simples de ser executado.

segunda-feira, 18 de abril de 2011

Como testar quando o software não tem botões?

Texto que escrevi para o blog Bytes don't Bite!

Mencionei anteriormente da importância, para uma pessoa que trabalha com testes de software, de se ter conhecimentos sobre como desenvolver e ter uma visão do ambiente em que o software opera. Tais conhecimentos podem ser considerados facultativos para diversos profissionais de teste, mas em certos casos se tornam obrigatórios.

O que acontece, por exemplo, quando não existe uma UI para testar? Este cenário acontece quando o software desenvolvido trata-se de uma API (Application Programming Interface). O teste de API requer o desenvolvimento de uma aplicação que simule o uso real das chamadas.

Uma vez que se trata de testes de API, Josh Poley menciona que muitas técnicas diferentes serão sugeridas, dependendo do testador. Entretanto, o mesmo apresenta uma situação que funcionou bem para os testes do Sistema Operacional do Xbox.

Participei de dois projetos em que aplicações eram desenvolvidas para que o produto principal pudesse ser testado. No primeiro, o produto desenvolvido continha implementações de JSRs para um emulador de J2ME. Nos testes, então, eram criadas aplicações J2ME, que tinham a proposta de avaliar as classes definidas em cada especificação.

No outro projeto, foi criada uma API para comunicação com determinadas impressoras. Nesta aplicação, uma das funcionalidades era o envio de uma linguagem intermediária para API, que gerava um comando (PCL / PJL / HP-GL) correspondente e mandava para impressão. Para tal funcionalidade, era necessário o conhecimento das entradas e saídas esperadas (tanto da linguagem intermediária como do comando enviado para impressora). Os testes foram desenvolvidos em C++, simulando uma aplicação real que utilizaria a API.

Mas antes de começar a desenvolver os testes, é necessário dedicar um tempo para o planejamento do que será testado. Dependendo da complexidade da aplicação, um documento de testes pode auxiliar nessa atividade.

Já na criação dos testes, deve-se pensar como o ambiente pode ser preparado para identificar certas situações, como por exemplo, o uso de um arquivo corrompido ou a ausência de conexão de rede. A atividade de forçar falhas, em uma aplicação, pode ser considerada bem interessante. Uma vez que se detêm conhecimento para criar testes que forcem defeitos, o entusiasmo é enorme. Mas atenção, a prioridade deve ser dada ao fluxo principal, primeiro crie testes para as situações em que se espera que o software funcione.

Alguns cenários são mais complexos de serem testados. Casos como atualização de estruturas de dados, modificação de certos recursos e chamadas da API que não possuem retorno, necessitam de alguma forma de verificação. Uma função que não tem retorno, por exemplo, deve mudar de alguma forma o estado da aplicação. Em um teste a uma chamada que remove algum valor de uma lista e não tem valor de retorno, pode ser necessário usar de chamadas para percorrer a lista e verificar os valores, tendo assim certeza de que o valor solicitado foi removido.

James Whittaker e Alan Jorgensen relataram que, em teste de API, alguns problemas interessantes para os testadores são: garantir que a aplicação varia os parâmetros das chamadas de modo a expor possíveis falhas; gerar combinações interessantes de parâmetros; configurar o ambiente externo e os dados internos que afetam a API; variar a seqüência de chamadas da API para exercitar de várias formas as funcionalidades.

No blog de testes da Google, em um dos posts é mencionado que testes de chamadas isoladas da API não conseguem simular o cenário real. Para uma calculadora, testar as chamadas isoladamente pode ser efetivo. Mas para aplicações em que a ordem das chamadas pode interferir no resultado, devem ser testadas tanto as chamadas isoladas como combinações entre elas.

A organização dos testes é importante para facilitar a manutenção. Códigos bem escritos podem auxiliar no entendimento da equipe sobre o que está sendo testado. Então, caso possível, o uso de um guia com padrões de codificação é bem vindo. Dessa forma, a equipe irá se acostumar a escrever de forma que todos entendam. Quando o cenário de teste é complexo, a documentação no código ajuda a entender do que se trata.

A qualidade do código de teste pode influenciar diretamente na qualidade do produto testado. Erros nos testes podem omitir falhas importantes nos produtos.

Mas quem vigia os vigilantes? Quem testa o código de teste? Como garantir que os testes estão corretos? Quando se escreve códigos de teste, a atenção deve ser dobrada. Com isso é importante a cultura de revisão de código, programação em pares ou alguma outra forma que auxilie na diminuição de erros. Criar log da execução pode ajudar na identificação de problemas.

Pelo fato de os testes serem parecidos, a prática do copy & paste vai parecer tentadora. Mas evitar esta prática pode evitar vários problemas. Recomendo uma apresentação de Borba, para ter a visão dele sobre código duplicado. E, se em algum momento achar que o código de teste pode melhorar, melhore!

Nos projetos que trabalhei, a padronização dos testes foi um ponto importante, para transferência de conhecimento aos que precisavam entender o que estava sendo testado. Para o emulador de J2ME, foi criado um framework no qual as aplicações de teste deveriam ser desenvolvidas. Em ambos os projetos, certos testes precisavam de uma confirmação visual, mas ainda assim foi possível avaliar tais cenários, só demandava uma confirmação do testador. Nos testes foram contempladas variações de parâmetros e combinações de chamadas. Também foram utilizadas algumas técnicas de teste como pairwise, classe de equivalência, valores limite, etc. Os testes eram documentados e tanto o documento como o código passavam por um processo de inspeção. Sempre que possível, os testes eram projetados para necessitarem da menor intervenção humana, realizando o maior número de testes ao apertar um único botão.

terça-feira, 22 de março de 2011

Incentivando uma briga

Texto que escrevi para o blog Bytes don't Bite!

Por muitas vezes refleti se as pessoas, que trabalham com testes de software, devem ter habilidades de desenvolvimento. Não nego que possuir conhecimento do negócio é necessário para o profissional da área de testes. Mas, entender sobre a estrutura do que é testado, que antes de tudo é um software, também é importante.

James Whittaker, que já trabalhou com testes na Microsoft e atualmente trabalha na Google, escreveu um livro que demonstra algumas técnicas para se “quebrar” um software. As falhas exibidas são bem interessantes, e possíveis de serem atingidas em abordagem caixa-preta.

Como testador, pude experimentar algumas técnicas mencionadas no livro. Vou relatar sobre a aplicação de uma dessas técnicas em um projeto que trabalhei. No software desenvolvido, um usuário poderia criar menus que seriam acessados através de um servidor. Quando acessado um determinado item do menu, uma ação seria acionada. Uma das ações disponíveis era verificar o usuário que estava acessando o item e direcionar para outra ação.

Na imagem acima, quando um usuário A acessa o ITEM 2 do MENU, a ação 2 é chamada. Uma vez que a AÇÃO 2 identifica que é o usuário A, a AÇÃO 3 é acionada. Olhando para este exemplo, tendo em vista que uma ação de decisão envolve processamento, já é possível reproduzir a falha. A idéia é forçar um processo a ser executado várias vezes. Imagine uma ação parecida com a AÇÃO 2 no lugar da AÇÃO 3, apontando para AÇÃO 2 quando o usuário for A.

Agora, imagine o usuário A acessando o MENU pelo servidor. Ele tenta acessar uma vez o ITEM 2, vai para a AÇÃO 2, que o direciona para AÇÃO 3. A AÇÃO 3 o manda de volta para AÇÃO 2, que o manda mais uma vez para AÇÃO 3. Vai para AÇÃO 2… Deu pra entender que nunca vai parar? Bastaram algumas requisições feitas com o usuário A para ser necessário reiniciar o servidor, pois o serviço havia ficado indisponível.

No livro “How to Break Software”, James Whittaker demonstra falhas que podem ser provocadas tanto pela interface com o usuário como através de outro software. Ele ainda fala que muitos têm dificuldade em entender o ambiente em que o software funciona. Na experiência que tive com testes de software até o momento, me deparei com vários testadores que somente interagiam com a porção de botões, ou outros componentes visuais, que lhe eram oferecidos através de especificações. Tais testadores desconsideravam sistema de arquivos, componentes externos, sistema operacional, rede, relacionamento com outras funcionalidades, etc.

Com esse post, pretendo incentivar uma briga.

Testadores, aprendam onde os desenvolvedores deixam os bugs. Acreditem, boa parte das falhas de um software estão no código! Estudem formas diferentes de se encontrar os problemas. Entenda o sistema operacional, analise o código fonte do software, entenda de redes, estude as falhas dos frameworks utilizados, avaliem a segurança do produto (não usem a desculpa de que segurança é um requisito não-funcional), estudem tudo o que for válido para certificar a qualidade do software.

Desenvolvedores, vocês vão deixar? Vejam o que esses testadores poderão fazer com o código produzido por vocês! Se aproximem do pensamento dos testadores, vejam como eles agem. Desenvolvam o senso crítico. Dêem seu máximo para que falhas comuns não aconteçam. Estudem onde os bugs aparecem para poder evitá-los!

No final, essa disputa saudável, entre evitar e encontrar os bugs, é uma forma de se obter um software de qualidade e uma equipe em evolução contínua.