Como seria um novo idioma se fosse projetado do zero para facilitar o TDD?

9

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?

Geoff
fonte
Que tal um idioma que não precisa de TDD? blog.8thlight.com/uncle-bob/2011/10/20/Simple-Hickey.html
Trabalho
2
Nenhum idioma precisa de TDD. TDD é uma prática útil , e um dos pontos de Hickey é que só porque você faz o teste não significa que você pode parar de pensar .
Frank Shearar
O Desenvolvimento Orientado a Testes trata de acertar as APIs internas e externas e fazê-lo com antecedência. Daí em Java que é tudo sobre as interfaces - as classes reais são subprodutos.

Respostas:

6

Muitos anos atrás, montei um protótipo que tratava de uma pergunta semelhante; aqui está uma captura de tela:

Teste de botão zero

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.

Carl Manaster
fonte
2
Haha, isso é incrível! Na verdade, eu gosto bastante da ideia de reunir testes com o código. É bastante tedioso (embora haja boas razões) no .NET ter assemblies separados com namespaces paralelos para testes de unidade. Também facilita a refatoração, porque a movimentação de código move automaticamente os testes: P
Geoff
Mas você quer deixar os testes lá? Você os deixaria ativados para o código de produção? Talvez eles possam ser # ifdef'd para C, caso contrário, estamos vendo hits de tamanho de código / tempo de execução.
Mawg diz que restabelece Monica
É puramente um protótipo. Se fosse real, teríamos que considerar coisas como desempenho e tamanho, mas é muito cedo para se preocupar com isso, e se chegássemos a esse ponto, não seria difícil escolher o que deixar de fora. ou, se desejado, deixar as asserções fora do código compilado. Obrigado pelo seu interesse.
Carl Manaster
5

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:

class MessageSender
  def send
    # Do something with a side effect
  end
end

Digamos que temos um MessageSenderUser que usa uma instância do MessageSender:

class MessageSenderUser

  def initialize(message_sender)
    @message_sender = message_sender
  end

  def do_stuff
    ...
    @message_sender.send
    ...
    @message_sender.send
    ...
  end

end

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_stuffchamadas são enviadas duas vezes. Assim como você faria em um idioma estaticamente digitado, você pode criar um MessageSender falso que conta quantas vezes sendfoi chamado. Mas, diferentemente de uma linguagem de tipo estaticamente, você não precisa de nenhuma classe de interface. Você apenas cria e cria:

class MockMessageSender

  attr_accessor :send_count

  def initialize
    @send_count = 0
  end

  def send
    @send_count += 1
  end

end

E use-o em seu teste:

mock_sender = MockMessageSender.new
MessageSenderUser.new(mock_sender).do_stuff
assert_equal(mock_sender.send_count, 2)

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.

class MessageSenderUser

  def initialize
    @message_sender = MessageSender.new
  end

  def do_stuff
    ...
    @message_sender.send
    ...
    @message_sender.send
    ...
  end

end

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:

mock_message_sender = mock MessageSender
MessageSender.should_receive(:new).and_return(mock_message_sender)
mock_message_sender.should_receive(:send).twice.with(no_arguments)
MessageSenderUser.new.do_stuff

Esse é o teste completo. Se MessageSenderUser#do_stuffnão chamar MessageSender#sendexatamente 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.

Wayne Conrad
fonte
Ótima resposta! Vocês estão começando a me responder de volta às linguagens dinâmicas :) Acho que a digitação com patos é a chave aqui, o truque com .new também seria possivelmente em uma linguagem de tipo estaticamente (embora fosse muito menos elegante).
Geoff
3

Então, como seria uma linguagem se fosse projetada do zero para ser ótima para o TDD?

'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.

Caleb
fonte
Concordado, é uma pergunta muito difícil de formular bem. Eu acho que o que quero dizer é que as ferramentas de teste atuais para linguagens como Java / C # parecem que a linguagem está atrapalhando um pouco e que algum recurso extra / alternativo da linguagem tornaria toda a experiência mais elegante (por exemplo, não ter interfaces para 90 % das minhas aulas, somente aquelas em que faz sentido do ponto de vista do design de nível superior).
Geoff
0

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.

Patkos Csaba
fonte
11
Concordo que as interfaces são ótimas e um elemento chave de design. No entanto, no meu código, acho que 90% das classes têm uma interface e que existem apenas duas implementações dessa interface, a classe e as simulações dessa classe. Embora este seja tecnicamente exatamente o ponto das interfaces, não posso deixar de sentir que é deselegante.
Geoff
Não estou familiarizado com zombaria em Java e C #, mas, tanto quanto sei, um objeto zombado imita o objeto real. Freqüentemente faço injeção de dependência usando um parâmetro do tipo do objeto e enviando uma simulação para o método / classe. Algo como a função someName (AnotherClass $ object = null) {$ this-> anotherObject = $ object? : new AnotherClass; } Este é um truque frequentemente usado para injetar dependência sem derivar de uma interface.
Patkos Csaba
11
Definitivamente, é aqui que as linguagens dinâmicas têm vantagem sobre as linguagens do tipo Java / C # em relação à minha pergunta. Um mock típico de uma classe concreta criará realmente uma subclasse da classe, o que significa que o construtor da classe concreta será chamado, o que é algo que você definitivamente deseja evitar (existem exceções, mas elas têm seus próprios problemas). Uma simulação dinâmica apenas aproveita a digitação de patos, para que não haja relação entre a classe concreta e uma simulação. Eu costumava codificar muito em Python, mas isso foi antes dos meus dias no TDD, talvez seja hora de dar uma outra olhada!
Geoff