Há muitas coisas a serem consideradas quando se trata de qualidade de software. Certamente ele precisa ser bem escrito (lembre-se: “Qualquer tolo pode escrever um código que um computador possa entender. Bons programadores escrevem códigos que os humanos podem entender.” Martin Fowler) para facilitar a manutenção, mas de nada servirá se não atender aos requisitos do seu cliente. Algumas perguntas podem surgir dessas premissas, (e que certamente já nos assombraram pelo menos uma vez quando olhamos para aquela tela com com várias linhas de código, certo?! 😁), sendo provavelmente a principal delas sobre como alcançar tal feito. Existe algum tipo de “receita” que possamos seguir que nos ajude a otimizar o processo de desenvolvimento de forma que tenhamos essas características de um software bem feito?
Desenvolvedores conhecidos e experientes criaram maneiras de adequar esse processo para que alguns objetivos e suas especificidades possam ser alcançados: Domain-Driven Design, de Eric Evans, por exemplo, visa conduzir o desenvolvimento de tal forma que o desenvolvedor foque em, bem… expressar de maneira eficiente a essência do software sendo desenvolvido em seu domínio, o que, por sua vez, é completamente focado no negócio do cliente. Robert Martin (também conhecido como “Uncle Bob”) também faz uma declaração semelhante em seu livro, Clean Architecture (Arquitetura Limpa), em que ele menciona que um software bem escrito “deve gritar sua intenção” e, em seguida, ensinar técnicas sobre como alcançar isso em sua arquitetura, para que você possa entender o objetivo da aplicação, idealmente, em apenas uma rápida olhada pelo código e a maneira como ele está organizado.
Mas afinal, quando a aplicação estiver pronta, ela vai funcionar como o esperado? Haverá algum comportamento estranho que não pôde ser encontrado quando os testes de sanidade dos algoritmos escritos foram realizados pela primeira vez? Algum erro de cálculo? Alguma exceção que não era esperada? São exatamente esses tópicos que são abordados no livro de Kent Beck!
Test Driven Development é um livro que explica sobre como escrever software tendo em mente os casos de uso, mas ao mesmo tempo, não deixando de lado outras boas práticas que devemos levar em conta ao escrever código. Nas palavras de Beck, seu objetivo é escrever “código limpo que funciona”. Ele é dividido em três partes principais: The Money Example, The xUnit Example e Padrões para Desenvolvimento Orientado a Testes (Patterns for Test-Driven Development). Este texto será dividido em duas partes: A primeira se concentrará em um pequeno resumo de sua primeira parte, que dá uma ideia introdutória de como o TDD funciona e o aplica escrevendo um aplicativo que lida com cenários que lidam com conversão entre moedas. A segunda incluirá também alguns comentários sobre como tem sido para nós a experiência de aplicação de tais conceitos.
Se você desenvolve há algum tempo, deve ter ouvido falar do mantra “vermelho / verde / refactor”. Esta é uma das primeiras coisas descritas no livro, e são descritas da seguinte forma:
- “Vermelho: Escreva um pequeno teste que não passe e talvez nem mesmo compile no início
- Verde: Faça o teste funcionar rapidamente, cometendo os ‘pecados’ necessários no processo
- Refactor: Elimine toda a duplicação criada apenas para fazer o teste funcionar”
Fonte: https://quii.gitbook.io/learn-go-with-tests/
Este é basicamente o estilo de programação em que o livro se concentra na maior parte do tempo. Ao dar exemplos da aplicação desse mantra, ele prova que é possível escrever dessa forma. Adicionalmente, refere que podem ter um “efeito social”, como se segue:
- “Se a densidade de defeitos puder ser reduzida o suficiente, então a equipe de QA (Quality Assurance) pode mudar de trabalho reativo para trabalho proativo.” Isso basicamente significa que a equipe de controle de qualidade pode se concentrar mais em saber se o aplicativo atende aos requisitos de negócios, em vez de focar em cenários “mecânicos” para encontrar mensagens de erros ou exceções que aparecem de tempos em tempos
- Além disso, podemos sempre ter software entregável com novas funcionalidades, o que leva a novas relações comerciais com os clientes
- Se o número de surpresas causados por defeitos que acontecem repentinamente puder ser reduzido o suficiente, os gerentes de projeto poderão estimar com mais precisão e até mesmo envolver o cliente no processo de desenvolvimento
- Se a conversa técnica puder ser clara o suficiente, os engenheiros de software “podem trabalhar em colaboração minuto a minuto” em vez de colaboração diária ou semanal
A partir daí temos um plano e uma motivação para fazê-lo. O autor prossegue com o “The Money Example”. Basicamente, se trata de um aplicativo que deve ser capaz de lidar com a conversão das várias moedas existentes em dólares americanos. Para cada problema a ser resolvido, há uma lista de “coisas a fazer” que terá seus itens riscados à medida que cada item for resolvido. A cada capítulo o autor escreverá um teste que falha, e lentamente o faz funcionar (e sim, eu realmente quis dizer essa palavra. A abordagem adotada no livro é a escrita de cada pedaço de código em pequenos passos (“baby steps”). Alguém que já desenvolve há algum tempo pode, inclusive, até adivinhar quais são os próximos passos, e isto é feito propositalmente. O autor diz que o objetivo é o de tornar o desenvolvimento no mundo real tão fácil e previsível quanto. É claro que ele também menciona que você não precisa sempre escrever código desta maneira e pode dar passos maiores quando se sentir mais confortável com o TDD).
Um pequeno exemplo de como se aplica
Você deve estar se perguntando agora: certo, entendi! Mas… como seria isso e… por que, considerando que posso nem mesmo saber o que deve ser feito até começar a desenvolver de fato? Eu nunca fiz isso. Escrever um teste que nem compila a princípio parece, uh… estranho para mim.
Escrever o teste primeiro nos obriga a pensar sobre como nosso software deve se comportar e também nos faz pensar (e escrever) sobre o design de uma boa Application Program Interface – API. Alguns podem pensar que deve ser difícil começar com testes. O argumento de Beck sobre é que você nem deveria estar escrevendo o código de uma aplicação que vai direto para o ambiente de produção se você não souber quais são os requisitos e como ele deve ser implementado. Isso apenas levaria a um código mais propenso a erros e com uma interface que pode ser complicada de ser consumida. O código pode fazer sentido logo após ser escrito mas, assim que outro desenvolvedor (ou o desenvolvedor daquele código em si, em um momento posterior) começar a usá-lo para alguns cenários diferentes, ele(a) pode descobrir que aquele código não está bem estruturado ou com um bom design e, infelizmente, pode ser tarde demais.
Uma observação rápida: o exemplo abaixo é baseado no próprio livro (o teste em si foi escrito quase que da mesma maneira, com exceção do nome do método, que foi escrito com base no padrão MétodoSobTest_DescriçãoDoCenário_ResultadoEsperado), mas há algumas modificações que o tornam ligeiramente diferente daquela do livro, pois essa também é minha interpretação de como o TDD pode ser aplicado. Além disso, o autor usa Java para escrever os exemplos, enquanto os exemplos descritos aqui serão escritos em C#. De qualquer maneira, os conceitos são os mesmos.
Dito isto, partiremos do primeiro requisito da aplicação: a aplicação deve suportar a multiplicação entre moedas. Iniciamos dando ao software a capacidade de multiplicar moedas equivalentes.
[Fact]
public void Times_WithPositiveMultiplier_ShouldMultiplyCorrectly()
{
Dollar five = new Dollar(5);
five.Times(2);
assertEqual(10, five.Amount);
}
E é assim que começamos! Sem classe Dollar, sem construtor, sem a implementação do método Times… a aplicação não compila. Este é o passo “vermelho”.
Agora, vamos para a etapa “verde”. O teste deve compilar e deve passar, mesmo que o código seja algo extremamente simples, escrito tão somente para que o teste passe:
public class Dollar
{
public Amount { get; set; }
public Dollar(int amount) {}
public void Times(int multiplier)
{
Amount = 5 * 2;
}
}
O teste compila e passa. Claro, o código não funcionaria em um ambiente de produção, pois cometemos “todos os pecados necessários no processo”. Agora fazemos a implementação correta (refactor): havia números mágicos, o construtor realmente não fazia nada e outros consumindo a classe Dollar poderiam definir qualquer valor para o campo Amount. Abaixo temos o código corrigido.
public class Dollar
{
public Amount { get; private set; }
public Dollar(int amount)
{
Amount = amount
}
public void Times(int multiplier)
{
Amount *= multiplier;
}
}
O teste e o código de produção devem funcionar corretamente agora. O campo Amount foi implementado como “somente leitura” para as classes externas que consumirão a classe Dollar e também o cálculo é realizado corretamente quando o método Times é invocado, atribuindo o resultado da operação para o campo Amount.
Value objects
Há mais no que se pensar. Poderia uma instância da classe Dollar ser considerada igual a outra desde seus valores para Amount sejam o mesmo? Sim, elas poderiam. Mas se você tiver diferentes instâncias, elas não serão consideradas iguais, mesmo que suas propriedades tenham os mesmos valores. Para isso, é necessário implementar uma classe para que considerem as propriedades que devem ser equivalentes entre as instâncias de um objeto para que eles sejam consideradas equivalentes. Este padrão é chamado de “Value Object”, e é realmente importante levá-lo em consideração ao implementar uma nova aplicação. A não implementação desse padrão pode causar bugs inesperados, dado que a comparação entra as instâncias não funcionará corretamente como o esperado. Para implementá-lo, deve-se fazer um override dos métodos Equals e GetHashCode (em C#) e, além disso, se as instâncias dessa classe forem usadas em Collections, você também precisará fazer com que a sua classe implemente a interface IEquatable<T> para que os métodos Linq (aprenda mais aqui, se você estiver interessado neste tópico), com o Distinct (que retorna outra coleção sem duplicatas) funcionem corretamente.
A mesma abordagem é adotada para considerar este novo caso. Não irei escrever aqui essas novas implementações orientadas a TDD aqui para que este texto não fique (ainda) maior do que já está (rs), mas você pode tentar implementá-las se quiser tentar exercitar a aplicação do TDD!
Triangulação
Outro tópico interessante que é introduzido neste livro é o conceito de Triangulação. O Dicionário de Cambridge (em inglês) define triangulação como sendo “a divisão de um mapa ou plano em triângulos para fins de medição, ou o cálculo de posições e distâncias usando este [mesmo] método”. Podemos traçar um paralelo e usá-lo quando não tivermos certeza de como refatorar ou como um determinado algoritmo para resolver um problema deve ser escrito. Você pode usar esta técnica para triangular isto. Para isso, você precisará de dois ou mais exemplos do que está tentando resolver. Por exemplo, digamos que você tenha uma instância de Dollar cujo Amount é 5, cujo nome será five. A partir disso, você também sabe que o seguinte é verdadeiro: five.Times(2) terá seu valor como 10, five.Times(3) será 15, five.Times(4) será 20. Com isso em mente, você pode inferir que a regra para o método Times será realmente o Amount vezes o argumento dele. Com essa implementação, não somente estaria apto a implementar um simples algoritmo para que o seu teste passasse, ao escrevê-lo primeiro, mas teria descoberto o padrão e, consequentemente, o algoritmo correto que estava procurando.
…
Esta foi uma introdução dos conceitos sobre TDD e sobre como podemos aplicá-los, juntamente com alguns outros tópicos interessantes introduzidos no livro. Na segunda parte deste texto, vamos discutir sobre a nossa experiência ao aplicar esses conceitos no desenvolvimento de software no mundo real. Continue acompanhando o nosso blog para não perder o próximo capítulo!


Leave a Reply