Para ajudar minha equipe a escrever código testável, eu vim com esta lista simples de práticas recomendadas para tornar nossa base de código C # mais testável. (Alguns dos pontos referem-se às limitações do Rhino Mocks, uma estrutura de simulação para C #, mas as regras também podem ser aplicadas de maneira mais geral.) Alguém tem alguma das melhores práticas que eles seguem?
Para maximizar a testabilidade do código, siga estas regras:
Escreva o teste primeiro, depois o código. Motivo: isso garante que você escreva um código testável e que cada linha de código receba testes escritos para ele.
Classes de design usando injeção de dependência. Motivo: você não pode zombar ou testar o que não pode ser visto.
Separe o código da IU de seu comportamento usando Model-View-Controller ou Model-View-Presenter. Motivo: permite que a lógica de negócios seja testada enquanto as partes que não podem ser testadas (a IU) são minimizadas.
Não escreva métodos ou classes estáticos. Motivo: os métodos estáticos são difíceis ou impossíveis de isolar e o Rhino Mocks não consegue zombá-los.
Programe interfaces, não classes. Motivo: o uso de interfaces esclarece as relações entre os objetos. Uma interface deve definir um serviço que um objeto precisa de seu ambiente. Além disso, as interfaces podem ser facilmente simuladas usando Rhino Mocks e outras estruturas de simulação.
Isole dependências externas. Motivo: as dependências externas não resolvidas não podem ser testadas.
Marque como virtuais os métodos que você pretende simular. Razão: Rhino Mocks não consegue simular métodos não virtuais.
fonte
Respostas:
Definitivamente, uma boa lista. Aqui estão algumas idéias sobre isso:
Eu concordo, em alto nível. Mas, eu seria mais específico: "Escreva um teste primeiro, depois escreva apenas o código suficiente para passar no teste e repita." Do contrário, temeria que meus testes de unidade se parecessem mais com testes de integração ou aceitação.
Acordado. Quando um objeto cria suas próprias dependências, você não tem controle sobre elas. Inversão de controle / injeção de dependência dá a você esse controle, permitindo que você isole o objeto em teste com mocks / stubs / etc. É assim que você testa objetos isoladamente.
Acordado. Observe que até mesmo o apresentador / controlador pode ser testado usando DI / IoC, entregando-lhe uma visualização e um modelo fragmentado / simulado. Confira Presenter First TDD para mais informações.
Não tenho certeza se concordo com este. É possível testar a unidade de um método / classe estática sem usar simulações. Então, talvez esta seja uma daquelas regras específicas do Rhino Mock que você mencionou.
Eu concordo, mas por um motivo um pouco diferente. As interfaces fornecem uma grande flexibilidade para o desenvolvedor de software - além de apenas suporte para vários frameworks de objetos fictícios. Por exemplo, não é possível oferecer suporte a DI corretamente sem interfaces.
Acordado. Oculte dependências externas por trás de sua própria fachada ou adaptador (conforme apropriado) com uma interface. Isso permitirá que você isole seu software da dependência externa, seja um serviço da web, uma fila, um banco de dados ou qualquer outra coisa. Isso é especialmente importante quando sua equipe não controla a dependência (também conhecida como externa).
Essa é uma limitação do Rhino Mocks. Em um ambiente que prefere stubs codificados à mão em vez de uma estrutura de objeto simulado, isso não seria necessário.
E, alguns novos pontos a serem considerados:
Use padrões de design criativos. Isso ajudará na DI, mas também permite que você isole esse código e teste-o independentemente de outra lógica.
Escreva testes usando a técnica Arrange / Act / Assert de Bill Wake . Essa técnica deixa bem claro qual configuração é necessária, o que está realmente sendo testado e o que é esperado.
Não tenha medo de fazer suas próprias simulações / tocos. Freqüentemente, você descobrirá que o uso de estruturas de objetos simulados torna seus testes incrivelmente difíceis de ler. Ao lançar o seu próprio, você terá controle total sobre seus mocks / stubs e poderá manter seus testes legíveis. (Consulte o ponto anterior.)
Evite a tentação de refatorar a duplicação de seus testes de unidade em classes de base abstratas ou métodos de configuração / desmontagem. Isso oculta o código de configuração / limpeza do desenvolvedor que está tentando interpretar o teste de unidade. Nesse caso, a clareza de cada teste individual é mais importante do que refatorar a duplicação.
Implementar integração contínua. Faça check-in do seu código em cada "barra verde". Construa seu software e execute seu conjunto completo de testes de unidade em cada check-in. (Claro, esta não é uma prática de codificação, por si só; mas é uma ferramenta incrível para manter seu software limpo e totalmente integrado.)
fonte
Se você estiver trabalhando com .Net 3.5, pode querer dar uma olhada na biblioteca de mocking do Moq - ela usa árvores de expressão e lambdas para remover idiomas de resposta de registro não intuitivos da maioria das outras bibliotecas de mock.
Confira este início rápido para ver como seus casos de teste se tornam mais intuitivos. Aqui está um exemplo simples:
// ShouldExpectMethodCallWithVariable int value = 5; var mock = new Mock<IFoo>(); mock.Expect(x => x.Duplicate(value)).Returns(() => value * 2); Assert.AreEqual(value * 2, mock.Object.Duplicate(value));
fonte
Saiba a diferença entre fakes, mocks e stubs e quando usar cada um.
Evite especificar interações usando simulações. Isso torna os testes frágeis .
fonte
Esta é uma postagem muito útil!
Eu acrescentaria que é sempre importante entender o Contexto e o Sistema em Teste (SUT). Seguir os princípios TDD ao pé da letra é muito mais fácil quando você está escrevendo um novo código em um ambiente em que o código existente segue os mesmos princípios. Mas quando você está escrevendo um novo código em um ambiente legado não TDD, você descobre que seus esforços de TDD podem aumentar rapidamente, muito além de suas estimativas e expectativas.
Para alguns de vocês, que vivem em um mundo inteiramente acadêmico, prazos e entrega podem não ser importantes, mas em um ambiente onde software é dinheiro, fazer uso eficaz de seu esforço de TDD é fundamental.
O TDD está altamente sujeito à Lei do Retorno Marginal Diminutivo . Em suma, seus esforços para TDD são cada vez mais valiosos até que você alcance um ponto de retorno máximo, após o qual, o tempo subsequente investido em TDD tem cada vez menos valor.
Tendo a acreditar que o valor principal do TDD está nos limites (caixa preta), bem como nos testes ocasionais da caixa branca de áreas críticas do sistema.
fonte
A verdadeira razão para programar em interfaces não é tornar a vida do Rhino mais fácil, mas esclarecer os relacionamentos entre os objetos no código. Uma interface deve definir um serviço que um objeto precisa de seu ambiente. Uma classe fornece uma implementação específica desse serviço. Leia o livro "Object Design" de Rebecca Wirfs-Brock sobre Funções, responsabilidades e colaboradores.
fonte
Boa lista. Uma das coisas que você pode querer estabelecer - e não posso lhe dar muitos conselhos, já que estou começando a pensar sobre isso sozinho - é quando uma classe deveria estar em uma biblioteca diferente, namespace, namespaces aninhados. Você pode até querer descobrir uma lista de bibliotecas e namespaces com antecedência e exigir que a equipe se reúna e decida fundir dois / adicionar um novo.
Oh, acabei de pensar em algo que faço e que talvez você também queira. Geralmente, tenho uma biblioteca de testes de unidade com um dispositivo de teste por política de classe, em que cada teste vai para um namespace correspondente. Eu também tenho outra biblioteca de testes (testes de integração?) Que está em um estilo mais BDD . Isso me permite escrever testes para especificar o que o método deve fazer, bem como o que o aplicativo deve fazer em geral.
fonte
Aqui está outro que pensei que gosto de fazer.
Se você planeja executar testes do teste de unidade Gui em vez de TestDriven.Net ou NAnt, achei mais fácil definir o tipo de projeto de teste de unidade para aplicativo de console em vez de biblioteca. Isso permite que você execute testes manualmente e passe por eles no modo de depuração (o que o TestDriven.Net mencionado anteriormente pode fazer por você).
Além disso, sempre gosto de ter um projeto Playground aberto para testar pedaços de código e ideias com as quais não estou familiarizado. Isso não deve ser verificado no controle de origem. Melhor ainda, ele deve estar em um repositório de controle de origem separado apenas na máquina do desenvolvedor.
fonte