Com algumas das linguagens mais comuns (Java, C #, Java etc.), às vezes parece que você está trabalhando em desacordo com a linguagem quando deseja TDD totalmente seu código.
Por exemplo, em Java e C #, você deseja zombar de quaisquer dependências de suas classes e a maioria das estruturas de zombaria recomendará que você zombe de interfaces e não de classes. Isso geralmente significa que você tem muitas interfaces com uma única implementação (esse efeito é ainda mais perceptível porque o TDD forçará você a escrever um número maior de classes menores). Soluções que permitem que você zombe de classes concretas corretamente fazem coisas como alterar o compilador ou substituir os carregadores de classe, etc., o que é bastante desagradável.
Então, como seria uma linguagem se fosse projetada do zero para ser ótima para o TDD? Possivelmente, de alguma maneira, no nível da linguagem, como descrever dependências (em vez de passar interfaces para um construtor) e poder separar a interface de uma classe sem fazê-lo explicitamente?
Respostas:
Muitos anos atrás, montei um protótipo que tratava de uma pergunta semelhante; aqui está uma captura de tela:
A idéia era que as asserções estivessem alinhadas com o próprio código e todos os testes fossem executados basicamente a cada pressionamento de tecla. Assim que você passa no teste, o método fica verde.
fonte
Seria dinamicamente, em vez de estaticamente digitado. A digitação de pato faria então o mesmo trabalho que as interfaces em idiomas de tipo estaticamente. Além disso, suas classes seriam modificáveis em tempo de execução para que uma estrutura de teste pudesse facilmente stub ou zombar de métodos em classes existentes. Ruby é uma dessas línguas; O rspec é sua principal estrutura de teste para TDD.
Como a digitação dinâmica ajuda no teste
Com a digitação dinâmica, você pode criar objetos simulados simplesmente criando uma classe que tenha a mesma interface (assinaturas de método) que o objeto colaborador que você precisa imitar. Por exemplo, suponha que você tenha alguma classe que enviou mensagens:
Digamos que temos um MessageSenderUser que usa uma instância do MessageSender:
Observe o uso aqui da injeção de dependência , um grampo dos testes de unidade. Voltaremos a isso.
Você deseja testar se as
MessageSenderUser#do_stuff
chamadas são enviadas duas vezes. Assim como você faria em um idioma estaticamente digitado, você pode criar um MessageSender falso que conta quantas vezessend
foi chamado. Mas, diferentemente de uma linguagem de tipo estaticamente, você não precisa de nenhuma classe de interface. Você apenas cria e cria:E use-o em seu teste:
Por si só, a "digitação de pato" de uma linguagem digitada dinamicamente não acrescenta muito aos testes em comparação com uma linguagem digitada estaticamente. Mas e se as classes não forem fechadas, mas puderem ser modificadas em tempo de execução? Isso é um divisor de águas. Vamos ver como.
E se você não tivesse que usar injeção de dependência para tornar uma classe testável?
Suponha que o MessageSenderUser apenas use o MessageSender para enviar mensagens, e você não precisa permitir a substituição do MessageSender por outra classe. Dentro de um único programa, esse geralmente é o caso. Vamos reescrever o MessageSenderUser para que ele simplesmente crie e use um MessageSender, sem injeção de dependência.
O MessageSenderUser agora é mais simples de usar: ninguém a criar precisa criar um MessageSender para ele usar. Não parece uma grande melhoria neste exemplo simples, mas agora imagine que o MessageSenderUser seja criado em mais de uma vez ou que possua três dependências. Agora, o sistema tem muitas instâncias de passagem apenas para fazer os testes de unidade felizes, não porque necessariamente melhora o design.
Classes abertas permitem testar sem injeção de dependência
Uma estrutura de teste em uma linguagem com digitação dinâmica e classes abertas pode tornar o TDD bastante agradável. Aqui está um trecho de código de um teste rspec para MessageSenderUser:
Esse é o teste completo. Se
MessageSenderUser#do_stuff
não chamarMessageSender#send
exatamente duas vezes, este teste falhará. A classe MessageSender real nunca é invocada: dissemos ao teste que sempre que alguém tenta criar um MessageSender, ele deve receber nosso MessageSender falso. Não é necessária injeção de dependência.É bom fazer muito em um teste tão simples. É sempre melhor não ter que usar injeção de dependência, a menos que isso faça sentido para o seu design.
Mas o que isso tem a ver com aulas abertas? Observe a chamada para
MessageSender.should_receive
. Não definimos #should_receive quando escrevemos o MessageSender, então quem o fez? A resposta é que a estrutura de teste, fazendo algumas modificações cuidadosas nas classes do sistema, é capaz de fazer com que ela apareça, já que #should_receive é definido em cada objeto. Se você acha que modificar classes de sistema como essa requer algum cuidado, você está certo. Mas é a coisa perfeita para o que a biblioteca de testes está fazendo aqui, e as classes abertas tornam isso possível.fonte
'funciona bem com TDD' certamente não é suficiente para descrever uma linguagem, portanto pode "parecer" qualquer coisa. Lisp, Prolog, C ++, Ruby, Python ... faça a sua escolha.
Além disso, não está claro que o suporte ao TDD seja algo que seja melhor tratado pelo próprio idioma. Claro, você pode criar uma linguagem em que cada função ou método tenha um teste associado e criar suporte para descobrir e executar esses testes. Mas as estruturas de teste de unidade já lidam bem com a parte de descoberta e execução, e é difícil ver como adicionar de forma limpa os requisitos de um teste para cada função. Os testes também precisam de testes? Ou existem duas classes de funções - normais que precisam de testes e funções de teste que não precisam delas? Isso não parece muito elegante.
Talvez seja melhor dar suporte ao TDD com ferramentas e estruturas. Construa-o no IDE. Crie um processo de desenvolvimento que o incentive.
Além disso, se você estiver criando um idioma, é bom pensar a longo prazo. Lembre-se de que o TDD é apenas uma metodologia, e nem a maneira preferida de trabalhar de todos. Pode ser difícil de imaginar, mas é possível que estejam chegando maneiras ainda melhores . Como designer de idiomas, você quer que as pessoas abandonem seu idioma quando isso acontecer?
Tudo o que você pode dizer para responder à pergunta é que esse idioma seria propício ao teste. Sei que isso não ajuda muito, mas acho que o problema está na questão.
fonte
Bem, linguagens de tipo dinâmico não requerem interfaces explícitas. Veja Ruby ou PHP, etc.
Por outro lado, linguagens de tipo estaticamente como Java e C # ou C ++ impõem tipos e obriga a escrever essas interfaces.
O que eu não entendo é qual é o seu problema com eles. As interfaces são um elemento-chave do design e são usadas em todos os padrões de design e no respeito aos princípios do SOLID. Por exemplo, uso frequentemente interfaces em PHP porque elas tornam o design explícito e também o impõem. Por outro lado, no Ruby você não tem como impor um tipo, é uma linguagem tipada por pato. Mas, ainda assim, é necessário imaginar a interface e abstrair o design em sua mente para implementá-lo corretamente.
Portanto, embora sua pergunta possa parecer interessante, isso implica que você tem problemas para entender ou aplicar as técnicas de injeção de dependência.
E, para responder diretamente à sua pergunta, Ruby e PHP têm uma ótima infraestrutura de zombaria, construída em suas estruturas de teste de unidade e entregues separadamente (consulte Mockery para PHP). Em alguns casos, essas estruturas permitem que você faça o que está sugerindo, como zombar de chamadas estáticas ou inicializações de objetos sem injetar explicitamente uma dependência.
fonte