Estou confuso sobre qual é a maneira correta de trabalhar com TDD


8

Estou tentando entender qual é a idéia por trás do TDD e como uma equipe deve trabalhar com ele. Eu tenho o seguinte caso de teste com NUnit + Moq (apenas escrevendo pela memória, não é garantido que o exemplo seja compilado, mas deve ser explicativo):

[Test]
public void WhenUserLogsCorrectlyIsRedirectedToLoginCorrectView() {
    Mock<IUserDatabaseRepository> repoMock = new Mock<IUserDatabaseRepository>();
    repoMock.Setup(m => m.GetUser(It.IsAny())).Returns(new User { Name = "Peter" });        

    Mock<ILoginHelper> loginHelperMock = new Mock<ILoginHelper>();
    loginHelperMock.Setup(m => m.Login(It.IsAny(), It.IsAny())).Returns(true);
    Mock<IViewModelFactory> factoryMock = new Mock<IViewModelFactory>();
    factoryMock.Setup(m => m.CreateViewModel()).Returns(new LoginViewModel());

    AccountController controller = new AccountController(repoMock.Object, loginHelperMock.Object, factoryMock.Object)

    var result = controller.Index(username : "Peter", password: "whatever");

    Assert.AreEqual(result.Model.Username, "Peter");
}

O AccountController tem 3 dependências, que eu zombo que, quando orquestradas dentro do controlador, permitem-me verificar se um login estava correto ou não.

O que me surpreende é que ... se no TDD, em teoria, você tiver que escrever primeiro seu conjunto de testes e criar seu código a partir dele, como devo saber de antemão que, para realizar minha operação, preciso usar essas três dependências e que a operação chamará determinadas operações? É como se eu precisasse conhecer as entranhas do Assunto em Teste antes mesmo de implementá-lo para zombar das dependências e isolar a classe, criando algum tipo de teste de gravação - código de gravação - teste de modificação, se necessário.

Naturalmente, sem nenhum conhecimento das entranhas do meu código e apenas expressando o teste, eu poderia expressá-lo como se fosse necessário apenas o ILoginHelper e "magicamente" supor antes de escrever o código que retornará o usuário em um login bem-sucedido (e, finalmente, perceba que a estrutura subjacente não funciona dessa maneira, por exemplo, retornando apenas um ID em vez do objeto completo).

Estou entendendo o TDD de maneira incorreta? Qual é uma prática típica de TDD em um caso complexo?

Obrigado


1
Você não precisa seguir o TDD rigoroso para obter o máximo benefício dos testes de unidade.
Den

2
@ Den: "TDD estrito" não significa o que o OP acredita que significa.
Doc Brown

Eu recomendo assistir vimeo.com/album/3143213/video/71816368 (8LU: Advanced Concepts in TDD). Isso pode ajudá-lo a entender as coisas.
Andrew Eddie

Respostas:


19

se em TDD, em teoria, você tem que escrever primeiro seu traje de teste e construir seu código a partir dele

Aqui está o seu mal-entendido. O TDD não se resume a escrever um conjunto completo de testes primeiro - isso é um mito falso. TDD significa trabalhar em pequenos ciclos,

  • escrevendo um teste de cada vez
  • implemente apenas o código necessário para tornar o teste "verde"
  • refator (o código e os testes)

Portanto, a criação de um conjunto de testes não é feita em uma única etapa, e não "antes da escrita do código", ela se entrelaça com a implementação do código em jogo.

Aplicado ao seu exemplo: você deve tentar começar com um teste simples para um controlador sem nenhuma dependência (algo como um protótipo). Então você implementa o controlador e refatora. Depois, você adiciona um novo teste que espera que seu controlador faça um pouco mais ou refatora / estende seu teste existente. Então você modifica seu controlador até que o novo teste fique "verde". Dessa forma, você começa com uma combinação simples de testes e assunto sob teste e termina com um teste complexo e sujeito sob teste.

Ao seguir esta rota, em algum momento você descobrirá quais dados adicionais são necessários como entrada para o controlador executar seu trabalho. Isso pode realmente acontecer no momento em que você tenta implementar um método de controlador, e não ao projetar o próximo teste. Esse é o ponto em que você para para implementar o método por um curto período de tempo e começa a introduzir as dependências ausentes primeiro (talvez por uma refatoração do construtor do seu controlador). Isso leva diretamente a uma refatoração de seus testes existentes - no TDD, você normalmente muda primeiro os testes que chamam o construtor e adiciona os novos atributos do construtor posteriormente. E é aí que a codificação e gravação dos testes se tornam completamente complicadas.


13

O que me surpreende é que ... se no TDD, em teoria, você tem que escrever primeiro seu traje de teste e construir seu código a partir dele, como devo saber de antemão que, para realizar minha operação, preciso usar essas três dependências e que a operação chamará determinadas operações? É como se eu precisasse conhecer as entranhas do Assunto em Teste antes mesmo de implementá-lo para zombar das dependências e isolar a classe, criando algum tipo de teste de gravação - código de gravação - teste de modificação, se necessário.

Parece errado, certo? E deveria - não porque seus testes para o controlador estejam incorretos ou "ruins" de qualquer forma, mas porque você deseja testar o controlador antes de ter qualquer coisa a ser "controlada". :)

O que quero dizer é: o TDD parecerá mais natural para você quando você começar a fazê-lo no nível de "regras de negócios" e "lógica real de aplicativos", que também é onde é mais útil. Os controladores geralmente lidam apenas com a delegação para outros componentes; portanto, é natural que, para testar se a delegação seja realizada corretamente, você precise saber para qual objeto ele será delegado. O único problema é quando você tenta fazer isso antes de ter qualquer lógica real implementada. Minha sugestão é que você tente implementar o LoginHelper, por exemplo, executando o TDD de maneira mais "orientada ao comportamento". Vai parecer mais natural e você provavelmente verá mais benefícios.

Então, para uma resposta mais genérica: TDD é uma prática com a qual produzimos testes antes de escrever o código que precisamos, mas não especifica que tipo de testes. Os controladores geralmente são integradores de componentes; portanto, você escreve testes de unidade que geralmente exigem muita zombaria. Ao escrever a lógica do aplicativo (regras de negócios, como fazer um pedido, validar autenticação do usuário, etc ...), você escreve testes de comportamento, que geralmente são testes baseados em estado (entrada fornecida versus saída desejada). Essa diferença é frequentemente chamada de mockismo versus estatística pela comunidade TDD. Faço parte do (pequeno) grupo que insiste em que os dois lados estão corretos, é apenas que eles oferecem compensações diferentes, sendo úteis para diferentes cenários, como os descritos acima.


1
Sua resposta tem alguns pontos positivos, mas permita-me escolher uma coisa. "Os controladores geralmente são integradores de componentes, então você escreve testes de integração, que geralmente exigem muita zombaria" - bem, acho que você provavelmente quis dizer "quando você tenta escrever testes de unidade para controladores, isso geralmente exige muita zombaria" . IMHO o termo "teste de integração" se encaixa melhor para um teste sem zombaria, onde você realmente usa os componentes reais, e não zomba, para ver se eles funcionam juntos conforme o planejado.
Doc Brown

Obrigado @DocBrown, eu estava realmente me referindo a um "teste de unidade que testa integração / comunicação entre componentes", e não ao conceito de testes de integração que incluem os componentes reais.
precisa saber é o seguinte

1
Bem, agora que concordamos com o termo "teste de integração", acho que sua resposta leva diretamente à próxima pergunta: vale a pena usar o TDD (ou escrever testes de unidade) para controladores com o papel principal de "integradores"? Ou deve-se preferir escrever apenas testes de integração para esses componentes (talvez depois)?
Doc Brown)

4

Embora o TDD seja um método de teste primeiro, ele não exige que você gaste muito tempo escrevendo código de teste antes de escrever qualquer código de produção.

Neste exemplo, a idéia de TDD descrita no livro seminal de Kent Beck sobre TDD ( 1 ) é começar com algo realmente simples, como talvez

AccountController controller = new AccountController()

var result = controller.Index(username : "Peter", password: "whatever");

Assert.AreEqual(result.Model.Username, "Peter");

No começo, você não sabe tudo o que precisará para fazer o trabalho. Você apenas sabe que precisará de um controlador com um método Index que fornece um modelo com um nome de usuário. Você ainda não sabe como isso será feito. Você acabou de definir uma meta para si mesmo.

Então você faz isso funcionar usando todos os meios disponíveis, possivelmente apenas codificando o resultado correto no início. Em refatorações subseqüentes (e até adicionando testes adicionais), você adiciona maior sofisticação, uma etapa de cada vez. O TDD permite que você dê o menor passo possível, mas também deixa você livre para dar o maior passo que suas habilidades e conhecimentos permitirem. Ao fazer um pequeno ciclo entre o código de teste e o código de produção, você obtém feedback sobre cada pequeno passo que você dá e sabe com quase imediato se o que você acabou de fazer funcionou e se ele quebrou qualquer outra que estava funcionando antes.

Robert Martin em ( 2 ) também defende um tempo de ciclo muito curto entre escrever código de teste e escrever código de produção.


3

Você pode precisar de toda essa complexidade para um teste de unidade conceitualmente simples, mas quase certamente não escreverá o teste dessa maneira em primeiro lugar.

Antes de tudo, a configuração complexa em suas seis primeiras linhas deve ser fatorada em um código de acessório reutilizável e independente. Os princípios da programação sustentável se aplicam ao código de teste, assim como o código comercial; se você usar o mesmo equipamento para dois ou mais testes, ele deve ser refatorado definitivamente em um método separado, para que você tenha apenas uma linha de distração no teste ou no código de configuração da classe para não ter nenhum.

Mas o mais importante: escrever um teste primeiro não garante que ele possa permanecer inalterado para sempre . Se você não conhece os colaboradores de uma chamada de método, quase certamente não será capaz de adivinhar que eles estão corretos na primeira tentativa. Não há nada errado em refatorar o código de teste junto com o código comercial se a API pública for alterada. É verdade que o objetivo do TDD é escrever a API correta e utilizável em primeiro lugar, mas isso quase nunca é alcançado em 100%. Requisitos sempremudar após o fato e, com muita freqüência, isso exige absolutamente colaboradores que não existiam quando você escreveu a primeira iteração de uma história. Nesse caso, não há nada a ser feito, a não ser ir além e alterar os testes existentes junto com seu aplicativo; e essas são as ocasiões em que a maioria do código de configuração que você cita entra no seu conjunto de testes.


2
Discordo totalmente da primeira parte. Os testes devem ser independentes. Esse é um requisito muito mais alto em testes de unidade do que em código, pois a independência melhora a capacidade de manutenção de testes de unidade, enquanto a falta de reutilização prejudica o código de produção.
Telastyn

1
Os testes @Telastyn ainda podem ser independentes ao compartilhar o código de configuração. Você só precisa ter certeza de usar um dispositivo novo , o que significa chamar um método de instalação compartilhado ou usar a instalação implícita (se sua estrutura de teste suportar).
Benjamin Hodgson

1
@BenjaminHodgson - Não vejo como um método de instalação compartilhado pode ser alterado para um teste e não para outro.
Telastyn

1
@ Telastyn Mas isso se aplica ao código reutilizado em geral - uma vez que uma classe tem mais de um cliente, é mais difícil mudar. Você está defendendo a duplicação de copiar e colar o código de configuração do aparelho em todos os testes de unidade?
Benjamin Hodgson

3
@Telastyn: se tornar os testes independentes entre si viola o princípio DRY, você inevitavelmente terá problemas ao tentar melhorar o design do seu código, mas precisa alterar 30 métodos de teste com "configuração semelhante" em vez de um método de instalação reutilizado . Esse é realmente o argumento principal que ouço com frequência contra o TDD - muito esforço para alterar os testes durante a refatoração - mas é quase sempre o problema dos testes não serem SECO o suficiente.
Doc Brown

2

É como se eu precisasse conhecer as entranhas do Assunto em Teste antes mesmo de implementá-lo para zombar das dependências e isolar a classe, criando algum tipo de teste de gravação - código de gravação - teste de modificação, se necessário.

Sim, até certo ponto você faz. Então, eu não acho que você esteja entendendo mal como o TDD funciona.

O problema é que - como outros já mencionaram - parece muito estranho a princípio, quase errado fazê-lo dessa maneira. Na minha opinião, isso mostra o que sinto ser o maior benefício do TDD: você precisa entender corretamente o requisito antes de escrever o código.

Como programadores, gostamos de escrever código. Portanto, o que nos parece "certo" e "natural" é examinar os requisitos e ficar preso o mais rápido possível. Os problemas de design tornam-se gradualmente aparentes à medida que você cria e testa a base de código. Então você as refatora e as corrige, e as coisas melhoram gradualmente e avançam em direção ao seu objetivo.

Embora divertido, essa não é uma maneira particularmente eficiente de fazer as coisas. É muito melhor ter uma noção adequada do que um módulo de software deve fazer primeiro, faça os testes e depois escreva o código. É menos refatoração, menos manutenção de teste e obriga a uma melhor arquitetura fora do bloco.

Eu não faço muito TDD e acho que o mantra "100% de cobertura de código" não faz sentido. Especialmente em casos como o seu. Mas adotar o TDD ainda tem muito valor, porque é uma vantagem em garantir que as coisas sejam bem projetadas e bem mantidas em todo o seu código.

Então, em resumo, o fato de você achar isso bizarro provavelmente é um bom sinal de que você está no caminho certo.


0

Zombar de dados é apenas a prática de usar dados fictícios. As estruturas do Moq tornam a criação de dados fictícios "mais fácil".

ARRANGE | ATO AFIRMAR

O TDD geralmente trata-se de criar seus testes e validar esses testes "aprovados". Inicialmente, o primeiro teste falhará, pois o código para validar esse teste ainda não foi criado. Acredito que esse seja realmente um certo tipo de teste; teste "vermelho / verde", que tenho certeza que é a fonte dos métodos "testados" hoje.

Geralmente, os testes validam as pequenas pepitas da lógica que fazem o código de imagem maior funcionar. Você pode começar no menor nível de função e, em seguida, avançar para as funções mais complicadas.

sim, algumas vezes a configuração ou "simulação" será um pouco intensa, e é por isso que usar uma estrutura moq é uma boa ideia; no entanto, se você se concentrar na lógica principal dos negócios, seus testes resultarão em uma garantia benéfica de que funciona conforme o esperado e pretendido.

Pessoalmente, não testo meus controladores porque tudo o que o controlador está usando foi testado para funcionar e, geralmente, não precisamos testar a estrutura.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.