Introdução ao Test Driven Development e Behavior Driven Development

Este artigo introduzirá o Test Driven Development e Behavior Driven Development, paradigmas relacionados aos testes e que aumentarão ainda mais a expressividade dos seus testes unitários.

Artigo no estilo: Curso
Fique por dentro
Este artigo, dividido em duas partes, irá mostrar técnicas que poderão ser aplicadas nos testes unitários e que facilitarão sua legibilidade e manutenções futuras, efetivamente tornando-os uma fonte de documentação concisa para os componentes da sua aplicação. Nesta primeira parte, abordaremos técnicas relacionadas à organização estrutural do código, nomenclaturas adequadas de métodos e variáveis, padrões e ferramentas. Este artigo também introduzirá o Test Driven Development e Behavior Driven Development, paradigmas relacionados aos testes e que aumentarão ainda mais a expressividade dos seus testes unitários.

Os testes unitários nos possibilitam exercitar os métodos, classes e componentes que compõem o nosso sistema e verificar que se comportam da maneira esperada. Através de uma suite de testes eficiente e de execução rápida, temos uma camada de segurança que nos permite refatorar, acrescentar ou alterar componentes sem que sejam introduzidas falhas em outros pontos do sistema. Adicionalmente, a sua presença serve como uma documentação do funcionamento das partes que compõem a aplicação.

A ideia em si de exercitar e testar os comportamentos do código sendo escrito não é algo novo, porém, a sua consolidação no processo de desenvolvimento de software ocorreu com a criação dos chamados frameworks xUnit. Originalmente concebidos para a linguagem de programação Smalltalk por Kent Beck, um dos idealizadores do Extreme Programming, estas ferramentas permitem a execução de partes isoladas do código da aplicação a fim de verificar se estão implementadas corretamente.

O surgimento do JUnit, uma adaptação do framework xUnit para o Java, foi importante porque a comunidade desta linguagem, numerosa e ativa, promoveu a discussão aberta e frequente de práticas e técnicas a serem aplicadas nos testes, aperfeiçoando a qualidade de suas implementações.

Destas discussões surgiu a percepção de que os testes unitários, apesar de não fazerem parte do código utilizado pelo usuário final, devem receber os mesmos cuidados que temos com o desenvolvimento de software em geral, como reuso de código e nomenclatura adequada de métodos, classes e variáveis.

Outra inovação originada na comunidade é a de que os testes unitários podem ser utilizados para o design do código da aplicação. Esta prática, conhecida como Test Driven Development, ou simplesmente TDD, permite que os testes nos ajudem a definir como iremos modelar as classes que compõem o sistema, como elas interagem entre si, e quais as assinaturas de seus métodos. A sua utilização no desenvolvimento de software trouxe impactos significativos em como abordamos o design de classes e, por isto, a sua análise e estudo são importantes. Com base nisto, este artigo irá introduzi-lo e também apresentar técnicas que podem ser aplicadas nos testes unitários, tornando-os muito mais legíveis e de fácil manutenção. Além disto, iremos explicar o funcionamento dos frameworks de mocks e stubs, que podem ser empregados para facilitar os testes de classes que têm muitas dependências.

Testando dependências externas através de Mocks e Stubs

Quando desenvolvemos uma aplicação, eventualmente criamos e/ou utilizamos componentes responsáveis por tarefas específicas ou acessamos sistemas de terceiros que, em conjunto, proveem uma funcionalidade desejada. Para testar esta funcionalidade, assim como validá-la, às vezes é necessário integrar estas partes, porém isso nem sempre é algo fácil de conseguir.

Uma abordagem inicial seria a instanciação manual e configuração destas partes, sendo que, para isto, precisaríamos fazer a configuração de todas as dependências necessárias, como conexões com o banco de dados, interações com sistemas externos e outras instâncias de classes. Além da dificuldade em realizar estes passos, também lidamos com a possibilidade destes componentes demorarem muito para responder, aumentando assim o tempo de execução dos testes, ou falharem. Outro problema está em definir o comportamento exato destas dependências como, por exemplo, quando precisamos verificar um cenário em que o web service de uma empresa terceira retorna um erro HTTP 500. Estas características de imprecisão e incerteza são indesejáveis para uma suite de testes unitários.

Precisamos então de outra forma de testar estas integrações, e uma das soluções é simulá-las através de objetos falsos ou, como são mais conhecidos, mocks e stubs. Estes nos disponibilizam formas de, programaticamente, controlar o retorno que gostaríamos de ter de um componente que faz parte de nossa aplicação, como quando devem lançar um erro ou retornar um código de erro. Na linguagem Java, os frameworks mais utilizados desta categoria são o Mockito e o EasyMock.

Podemos observar nas Listagens 1 e 2 exemplos de testes empregando o EasyMock e o Mockito, respectivamente. Nele, descrevemos um sistema bancário simples, representado pela classe ServicoDePagamentos e seu método transferirDe(), em que o usuarioOrigem deseja transferir um valor monetário, simbolizado pela variável valorASerTransferido, para o usuarioDestino, sendo necessário para isto verificar se há saldo suficiente para realizar a operação.

Listagem 1. Usando o EasyMock para simular o comportamento de um sistema terceiro. @Before public void init(){ servicoDePagamentos = new ServicoDePagamentos(); // criando o mock de um componente, o repositorioDePagamentos repositorioDePagamentos = createMock(RepositorioPagamentos.class); //setando o mock na instância de ServicoDePagamentos servicoDePagamentos.setRepositorioDePagamentos (repositorioDePagamentos); } @Test public void testTransferenciaComSucesso() throws Exception{ Usuario usuarioOrigem = new Usuario("aaa@bbb.com"); Usuario usuarioDestino = new Usuario("bbb@ccc.com"); BigDecimal valorASerTransferido = new BigDecimal("90"); // Informando ao framework de mock o comportamento do // repositorioDePagamentos expect(repositorioDePagamentos.procurarSaldo (usuarioOrigem)).andReturn( valorASerTransferido.add(BigDecimal.ONE)); repositorioDePagamentos.adicionarSaldo(usuarioOrigem, valorASerTransferido.negate()); expectLastCall().once(); repositorioDePagamentos.adicionarSaldo(usuarioDestino, valorASerTransferido); expectLastCall().once(); // informando ao EasyMock que já configuramos todo o comportamento // esperado na instância do mock; replay(repositorioDePagamentos); servicoDePagamentos.transferirDe(contaDeOrigem, contaDeDestino, valorASerTransferido); // verifica se todos os comportamentos configurados foram invocados verify(repositorioDePagamentos); }

Listagem 2. Usando o Mockito para simular o comportamento de um sistema terceiro. @Before public void init(){ servicoDePagamentos = new ServicoDePagamentos(); //criando o mock de um componente, o repositorioDePagamentos repositorioDePagamentos = mock(RepositorioPagamentos.class); //setando o mock na instância de ServicoDePagamentos servicoDePagamentos.setRepositorioDePagamentos (repositorioDePagamentos); } @Test public void testTransferenciaComSucesso() throws Exception{ Usuario usuarioOrigem = new Usuario("aaa@bbb.com"); Usuario usuarioDestino = new Usuario("bbb@ccc.com"); BigDecimal valorASerTransferido = new BigDecimal("90"); // informando ao framework de mock o comportamento do // repositorioDePagamentos when(repositorioDePagamentos.procurarSaldo (usuarioOrigem)).thenReturn( valorASerTransferido.add(BigDecimal.ONE)); servicoDePagamentos.transferirDe(usuarioOrigem, usuarioDestino, valorASerTransferido); //verifica se os saldos foram corretamente transferidos verify(repositorioDePagamentos).adicionarSaldo(usuarioOrigem, valorASerTransferido.negate()); verify(repositorioDePagamentos).adicionarSaldo(usuarioDestino, valorASerTransferido); }"

[...] continue lendo...

Artigos relacionados