Dúvida com um exercício de TDD

Olá pessoas,

Para praticar com algumas ferramentas de GUI estou criando uma calculadora (não é uma calculadora comum, mas só vou dar detalhes quando estiver pronto :slight_smile: )

A parte do “modelo”, que eu chamei de Engine, é onde está toda a lógica de cálculos, controle de estados conforme o pressionamento das teclas, formatação dos números em bases diferentes, etc. Pois bem, essa parte eu resolvi programar usando TDD, e já comecei empacando um pouco.

A API desse engine seria basicamente o seguinte:

  • Um método “input” é chamado pela GUI a cada tecla digitada, e a engine é responsável por concatenar esse valor ao que ele já tem. Por exemplo, o valor atual é “70” ; o usuário pressiona o “1”, a GUI chama engine.input(“1”) e o valor interno passa a ser “701”
  • Esse valor internamente é armazenado como número, mas não há necessidade de expor na API.
  • Outros métodos são chamados pela GUI na renderização, ele traz os valores formatados na base desejada. São métodos do tipo “getValueBase10()” ou “getValueBase16()”

Penso que os primeiros testes devem abordar esse input/output básico, para só depois ir aos cálculos e manutenção de estados. Mas estou em um dilema: Se escrever um teste para o método de input, como vou testar o resultado sem chamar o método de output? Se escrever um teste para o método de output, como entrar com os dados? Devo deixar o campo com o valor interno (numérico) público para que os testes possam manipulá-lo? Seria correto em um teste usar como suporte um outro método da classe que está sendo testada - por exemplo, pegar o resultado formatado para validar o input?

Como desenrosco esse nó? Ou há algum erro além disso, e o melhor seria mudar a abordagem inicial?

Obrigado!

Sao muitas perguntas em um post tao curto, mas vou tentar te ajudar.

Voce pode iniciar TDD de duas formas diferentes: outside-in e inside-out.

Eu prefiro fazer inside-out, ou seja, implemento com testes a parte principal da funcionalidade e vou indo para fora aos poucos, assim preciso mockar pouca coisa. Eu construo meus objetos de negocio em memoria, preferencialmente sem ter que espalhar a criacao deles por diversos testes, normalmente uso um builder que reutilizo onde preciso. Assim eu implemento primeiro a regra, com testes, e nos componentes mais externos uso objetos reais, ja implementados e testados, só tendo que mockar um mínimo de coisas, como componente de mensagens, entity managers e coisas do genero.

Ha quem prefira construir outside-in: Começa pelas partes externas e vai indo em direçao a regra. Confesso que tenho menos experiência com essa técnica, mas nos exemplos que já vi é criada mais uma camada de abstração entre o seu “controller” e seus objetos de negócio. Então você mocka essa abstracao nos testes e faz com que essa “fachada” retorne o resultado esperado.

Não entendi bem essa pergunta. Você diz, criar um método na classe só pra ser usado no teste?

Obrigado por responder,

[quote=YvGa]

Não entendi bem essa pergunta. Você diz, criar um método na classe só pra ser usado no teste?[/quote]

O que quis dizer aqui foi usar um método do objeto testado em outro teste. Por exemplo, suponha que eu tenha uma classe que armazena uma data internamente, mas sua interface pública trabalha com string usando os métodos parse() e format(). Vou escrever um teste para o método parse()

    MyDate dt = formatter.parse("01/01/2013");  // Esse é o método que estou testando
    assertEquals("01/01/2013", dt.format());       // Para validar fiz uso de um outro método, que também faz parte da classe testada (e provavelmente está coberta em outro teste)

É disso que estou falando. Eventualmente eu poderia até cair aqui em uma espécie de loop infinito - por exemplo, não consigo testar o parse() sem validar a condição com o format(), e não consigo validar o format() sem inputar dados antes com o parse().
Como fazer isso, devo permitir acesso ao campo em que a data é armazenada internamente, apenas para que os métodos de teste possam enxergá-lo?

Não sei se consegui explicar bem o caso da minha aplicação, mas o dilema é parecido com esse exemplo das datas: também tenho um método de entrada de informação e um de saída, ambos com lógica que precisa ser testada. Internamente o valor está em um campo que a princípio não seria de interesse de outras classes acessá-lo diretamente.

[quote=gomesrod]Obrigado por responder,

Uma das possibilidades seria mockar uma interface que seu objeto implementaria, entao no “format” dele voce devolve o que quer, sem precisar "configurar’ seu teste novamente para testar outra coisa. Ou voce pode simplesmente configurar e testar outra coisa com uma mesma configuracao.

O que voce tem que ver é se existe realmente sentido nesse teste, se o que voce esta fazendo é realmente necessario testar. O caso do format, por exemplo, é um em que você não precisaria testar, você não precisa testar um metodo de outra api.

Talvez o exemplo não tenha ficado claro pra mim, mas a impressão que me passa é que você está testando algo que não precisaria ser testado, ou sua classe tem mais responsabilidades que deveria, pois um teste é um teste, se outro teste tem que testar a mesma coisa, antes de poder testar outra coisa, parece haver problema de acoplamento aí.

Nesse caso o format seria implementado pela minha classe.

É justamente esse tipo de coisa que estou tentando identificar.

Mas realmente não expliquei bem os exemplos, ficou tudo bastante confuso e vou recomeçar do zero.

Esqueça essa classe de datas, só serviu para complicar as coisas; vamos trabalhar com um exemplo parecido com o que estou realmente tentando desenvolver.

A classe em questão contém as “regras de negócio” de uma calculadora. Vou chamá-la de CalculatorEngine.

Suas funcionalidades são:

  • Receber os valores inputados na GUI.

  • Esses valores podem estar em Decimal, Binário ou Hexa. O usuário escolherá a base em que vai fazer o input.

  • Armazenar o valor internamente em formato de número - Esse valor nunca será acessado diretamente pela GUI, portanto imagino que é um atributo privado.

  • Fornecer o valor em Decimal, Binário ou Hexa - Assim é que a GUI terá acesso ao valor para exibir ao usuário.

  • Além das funcionalidades de cálculo… mas isso vai ficar para a próxima etapa, por enquanto eu quero que funcione apenas como um conversor de base.

A API é mais ou menos assim (eu sei que no TDD é pecado ter código antes do teste, mas foi o único jeito de mostrar de maneira mais precisa o que estou imaginando)

class CalculatorEngine {
       private long currentValue; // O valor será armazenado aqui

       // A GUI chamará esse método para informar a base escolhida pelo usuário (binário, decimal ou hexa)
       void setInputBase(InputBase base) { ... }  

       // Quando o usuário pressionar alguma tecla a GUI chama esse método.
       // O valor será interpretado de acordo com a base escolhida.
       void input(String val) { ... }

       // Devolve os valores convertidos para serem exibidos pela GUI.
       String getValueAsDecimal() { ... }
       String getValueAsBinary() { ... }
       String getValueAsHexadecimal() { ... }
}

E o dilema é: como testar o método input() sem usar os getValue() para verificar o valor inputado ? E como testar os métodos de conversão (getValue) sem inserir valores com input() ?
Isso é o que quis dizer, o teste de um método depende de outro método e por isso pode ficar comprometido.

Isso se resolveria tornando visível o número real (atributo currentValue), mas para isso queria ter certeza que é aceitável quebrar o encapsulamento por causa dos testes unitários.

Ou ainda não entendi o seu exemplo ou não existe problema nenhum.

É normal você usar um método e depois testar o estado através de outro, ou testar o estado do seu objeto usando outros como input. Talvez ainda não tenha conseguido entender a sua duvida, mas a principio não me parece haver problema algum.

Se eu entendi direito, também tenho geralmente a mesma dúvida que você.
Aliás, quanto mais leio sobre OO, príncipios como “tell, don’t ask”, mais vejo dificuldade em testar esse tipo de coisa.

Pela sua explicação, seu atribute currentValue não precisa ser visível fora do objeto.
Então para ter acesso a ele, você sempre terá que usar método que retorne seu valor após ser aplicada alguma lógica.

Essa lógica pode inclusive aplicar alguma transformação, então na prática você estaria testando dois pedaços de lógica da mesma vez.

Algumas soluções que vejo por aí pra isso:

  • Relaxar o encapsulamento para permitir o teste: deixar um método protected, por exemplo.

  • Usar mocks para monitorar as interações do seu objeto.

  • Testar as duas lógicas ao mesmo tempo.

Sinceramente não gosto muito de nenhuma delas, mas na prática acabo me adaptando a situação.

No seu exemplo específico, acho que pode estar atribuindo muitas responsabilidades para sua Engine e com classes menores ficasse mais simples.

Por exemplo: A base dos números, poderia ser apenas um conversor de entrada e saída, sendo transparente para sua classe.
Com isso você poderia expor diretamente o currentValue para o conversor ou injetar o conversor na Engine (passando um mock para testar o currentValue).

O que acha?

É aí mesmo que eu queria chegar… as soluções que me vieram à cabeça foram a primeira e a terceira. Segundo seu comentário não são o ideal, mas também não são nenhum crime e podem ser aplicadas dependendo do caso, certo?

[quote=YvGa]Ou ainda não entendi o seu exemplo ou não existe problema nenhum.

É normal você usar um método e depois testar o estado através de outro, ou testar o estado do seu objeto usando outros como input. Talvez ainda não tenha conseguido entender a sua duvida, mas a principio não me parece haver problema algum.[/quote]
Se você não viu nenhum problema escancarado, então vou considerar que estava indo por um caminho não-tão-mau-assim :slight_smile: Mas em outro post vc levantou a suspeita de que havia um excesso de responsabilidades e acho que esse é o caso, veja logo abaixo.

Acho que aqui o quebra-cabeça se encaixa. Aquela sensação de que havia algo errado, excesso de responsabilidade, mas sem conseguir explicar exatamente por que… Era isso!
Os conversores deveriam ser objetos à parte! Parece que matou o problema, vou dar uma olhada mas creio que esse seja o caminho a ser seguido.

Quando eu disse sobre as responsabilidades eu estava falando sobre o seu metodo getValueAsDecimal(), por exemplo, delegar a sua implementação “as decimal” para uma classe especialista nisso e simplesmente utilizá-la. Veja que isso diminui a necessidade dos testes na classe que apenas usaria esse “AsDecimalzador”, ou seja na classe que possui o metodo getValueAsDecimal() você não precisa testar a conversão propriamente dita, já que ela é delegada, precisando testar apenas exceções ou fluxos diferentes que essa conversão venha a obrigar. Para isso, caso seja simples você pode configurar “AsDecimalzador” para testar seu getValueAsDecimal(), mas sem se preocupar em testá-lo nesse momento, ou pode mockar e retornar o esperado para suas exceções e fluxos diferentes.

Não sei se me fiz entender ou compliquei mais ainda.

Eu não vejo problema de usar o método input e o método getValueAsDecimal num mesmo teste.

Se o valor de currentValue é um detalhe de implementação, você não deve checá-lo com os testes unitários. Se você fizer isto, ao alterar a representação interna do objeto (a implementação), seus testes quebram. Se você considerar que no TDD os testes são uma especificação, não é interessante que eles sejam dependentes de detalhes de implementação. O método input altera o estado do seu objeto. Você precisa checar se o estado foi alterado adequadamente. Para isto, use um método disponível na sua interface pública. getValueAsDecimal (ou outro semelhante) podem resolver o teu problema.

A unidade (do unit test) nem sempre representa um único método: testar o método input em isolado não dá, bem como testar o getValueAsDecimal em isolado também não.

Usar um conversor separado pode ser interessante, pois te permite testar as operações de conversão separadamente. Mas o método input continua lá e você não testou se ele de fato alterou o comportamento do objeto.

Pensando em OO, currentValue é uma propriedade do objeto, assim como getValueAsDecimal. Por que você não veria problemas em checar o valor do primeiro, mas acha ruim checar o segundo?

[quote=YvGa]Quando eu disse sobre as responsabilidades eu estava falando sobre o seu metodo getValueAsDecimal(), por exemplo, delegar a sua implementação “as decimal” para uma classe especialista nisso e simplesmente utilizá-la. Veja que isso diminui a necessidade dos testes na classe que apenas usaria esse “AsDecimalzador”, ou seja na classe que possui o metodo getValueAsDecimal() [/quote]É isso mesmo, vou alterar para que todos os conversores “AsDecimalzador” “Binarizador” e “Hexadecimalzador” sejam classes separadas.

Verdade, mas acho que não dá para fugir disso… agora que os conversores estão em um objeto à parte preciso de uma maneira de obter o valor “puro” que está no objeto; provavelmente vai ser necessário também incluir esse acesso na interface pública da classe.

[quote=wagnerfrancisco]Pensando em OO, currentValue é uma propriedade do objeto, assim como getValueAsDecimal. Por que você não veria problemas em checar o valor do primeiro, mas acha ruim checar o segundo? [/quote]É porque o segundo método tinha lógica (e seus próprios testes unitários), e o primeiro não; assim os testes ficavam meio amarrados, testando sempre os dois métodos juntos. Mas agora colocando os conversores à parte, o único acesso será pelo currentValue mesmo.