Você deve codificar seus dados em todos os testes de unidade?

33

A maioria dos tutoriais / exemplos de teste de unidade existentes geralmente envolve a definição dos dados a serem testados para cada teste individual. Eu acho que isso faz parte da teoria "tudo deve ser testado isoladamente".

No entanto, descobri que, ao lidar com aplicativos de várias camadas com muita DI , o código necessário para configurar cada teste fica muito longo. Em vez disso, criei várias classes de testbase que agora posso herdar, com muitos andaimes de teste pré-criados.

Como parte disso, também estou criando conjuntos de dados falsos que representam o banco de dados de um aplicativo em execução, embora geralmente apenas uma ou duas linhas em cada "tabela".

É uma prática aceita predefinir, se não todos, a maioria dos dados de teste em todos os testes de unidade?

Atualizar

A partir dos comentários abaixo, parece que estou fazendo mais integração do que testes de unidade.

Meu projeto atual é o ASP.NET MVC, usando o Unit of Work sobre Entity Framework Code First e o Moq para teste. Eu zombei da UoW e dos repositórios, mas estou usando as classes reais de lógica de negócios e testando as ações do controlador. Os testes costumam verificar se a UoW foi confirmada, por exemplo:

[TestClass]
public class SetupControllerTests : SetupControllerTestBase {
  [TestMethod]
  public void UserInvite_ExistingUser_DoesntInsertNewUser() {
    // Arrange
    var model = new Mandy.App.Models.Setup.UserInvite() {
      Email = userData.First().Email
    };

    // Act
    setupController.UserInvite(model);

    // Assert
    mockUserSet.Verify(m => m.Add(It.IsAny<UserProfile>()), Times.Never);
    mockUnitOfWork.Verify(m => m.Commit(), Times.Once);
  }
}

SetupControllerTestBaseestá construindo o UoW falso e instanciando o userLogic.

Muitos testes exigem a existência de um usuário ou produto existente no banco de dados, por isso, preenchi previamente o que a UoW simulada retorna, neste exemplo userData, que é apenas um IList<User>registro de usuário único.

mattdwen
fonte
4
O problema com os tutoriais / exemplos é que eles precisam ser simples, mas você não pode mostrar a solução para um problema complexo em um exemplo simples. Eles devem ser acompanhados por "estudos de caso" descrevendo como a ferramenta é usada em projetos reais de tamanho razoável, mas raramente o são.
Jan Hudec 27/09/13
Talvez você possa adicionar alguns pequenos exemplos de código com os quais você não está totalmente satisfeito.
Luc Franken
Se você precisar de muito código de configuração para executar um teste, corre o risco de executar um teste funcional. Se o teste falhar quando você alterar o código, mas não houver nada errado com o código. É definitivamente um teste funcional.
Reactgular 27/09/13
O livro "xUnit Test Patterns" é um argumento forte para equipamentos e auxiliares reutilizáveis. O código de teste deve ser tão sustentável quanto qualquer outro código.
Chuck Krutsinger
Este artigo pode ser útil: yegor256.com/2015/05/25/unit-test-scaffolding.html
yegor256

Respostas:

25

Por fim, você deseja escrever o mínimo de código possível para obter o máximo de resultados possível. Ter muito do mesmo código em vários testes a) tende a resultar em codificação de copiar e colar eb) significa que, se uma assinatura de método for alterada, você poderá ter que corrigir muitos testes quebrados.

Eu uso a abordagem de ter classes TestHelper padrão que me fornecem muitos tipos de dados que uso rotineiramente, para que eu possa criar conjuntos de entidades padrão ou classes DTO para que meus testes consultem e saibam exatamente o que receberei a cada vez. Então, eu posso ligar TestHelper.GetFooRange( 0, 100 )para obter um intervalo de 100 objetos Foo com todas as suas classes / campos dependentes configurados.

Especialmente quando existem relacionamentos complexos configurados em um sistema do tipo ORM que precisam estar presentes para que as coisas funcionem corretamente, mas não são necessariamente significativos para este teste que podem economizar muito tempo.

Nas situações em que estou testando próximo ao nível dos dados, às vezes crio uma versão de teste da minha classe de repositório que pode ser consultada de maneira semelhante (novamente, isso ocorre em um ambiente do tipo ORM e não seria relevante em relação a um banco de dados real), porque zombar das respostas exatas às consultas é muito trabalhoso e, muitas vezes, oferece apenas pequenos benefícios.

Existem alguns cuidados a serem tomados, embora em testes de unidade:

  • Verifique se suas zombarias são zombarias . As classes que executam operações em torno da classe que está sendo testada devem ser objetos simulados se você estiver realizando testes de unidade. Suas classes de DTO / tipo de entidade podem ser reais, mas se as classes estão executando operações, você precisa estar zombando delas - caso contrário, quando o código de suporte for alterado e seus testes começarem a falhar, você precisará procurar muito mais para descobrir qual alteração realmente causou o problema.
  • Verifique se você está testando suas aulas . Às vezes, se examinarmos um conjunto de testes de unidade, fica aparente que metade dos testes está realmente testando a estrutura de zombaria mais do que o código real que eles deveriam estar testando.
  • Não reutilize objetos simulados / de suporte Isso é muito importante - quando alguém começa a se tornar esperto com testes de unidade de suporte a código, é realmente fácil criar inadvertidamente objetos que persistem entre os testes, o que pode ter efeitos imprevisíveis. Por exemplo, ontem eu tive um teste que passou quando executado por conta própria, passou quando todos os testes da classe foram executados, mas falhou quando todo o conjunto de testes foi executado. Aconteceu que havia um objeto estático furtivo em um auxiliar de teste que, quando eu o criei, definitivamente nunca causaria um problema. Lembre-se: no início do teste, tudo é criado, no final do teste tudo é destruído.
glenatron
fonte
10

O que quer que torne a intenção do seu teste mais legível.

Como regra geral:

Se os dados fizerem parte do teste (por exemplo, não devem imprimir linhas com um estado 7), codifique-os no teste, para que fique claro o que o autor pretendia que acontecesse.

Se os dados forem apenas de preenchimento para garantir que eles tenham algo com que trabalhar (por exemplo, não deve marcar o registro como completo se o serviço de processamento gerar uma exceção), tenha um método BuildDummyData ou uma classe de teste que mantenha os dados irrelevantes fora do teste .

Mas note que estou lutando para pensar em um bom exemplo deste último. Se você tem muitos deles em um equipamento de teste de unidade, provavelmente tem um problema diferente a resolver ... talvez o método em teste seja muito complexo.

pdr
fonte
+1 eu concordo. Isso cheira como o que ele está testando é fortemente acoplado para testes de unidade.
Reactgular
5

Diferentes métodos de teste

Primeiro defina o que você está fazendo: Teste de unidade ou teste de integração . O número de camadas é irrelevante para o teste de unidade, pois você provavelmente testa apenas uma classe. O resto você zomba. Para testes de integração, é inevitável que você teste várias camadas. Se você tiver bons testes de unidade, o truque é tornar os testes de integração não muito complexos.

Se seus testes de unidade forem bons, não será necessário repetir todos os detalhes ao realizar testes de integração.

Termos que usamos, esses são um pouco dependentes da plataforma, mas você pode encontrá-los em quase todas as plataformas de teste / desenvolvimento:

Exemplo de aplicação

Dependendo da tecnologia usada, os nomes podem diferir, mas usarei isso como um exemplo:

Se você possui um aplicativo CRUD simples com o modelo Product, ProductsController e uma visualização de índice que gera uma tabela HTML com produtos:

O resultado final do aplicativo está mostrando uma tabela HTML com uma lista de todos os produtos que estão ativos.

Teste de unidade

Modelo

O modelo que você pode testar com bastante facilidade. Existem métodos diferentes para isso; nós usamos luminárias. Eu acho que é isso que você chama de "conjuntos de dados falsos". Portanto, antes de cada teste ser executado, criamos a tabela e inserimos os dados originais. A maioria das plataformas possui métodos para isso. Por exemplo, na sua classe de teste, um método setUp () que é executado antes de cada teste.

Em seguida, executamos nosso teste, por exemplo: produtos testGetAllActive .

Então, testamos diretamente em um banco de dados de teste. Nós não zombamos da fonte de dados; nós sempre fazemos o mesmo. Isso nos permite, por exemplo, testar com uma nova versão do banco de dados, e quaisquer problemas de consulta surgirão.

No mundo real, você nem sempre pode seguir 100% de responsabilidade única . Se você quiser fazer isso ainda melhor, poderá usar uma fonte de dados que você zomba. Para nós (usamos um ORM) que parece testar a tecnologia já existente. Além disso, os testes se tornam muito mais complexos e realmente não testam as consultas. Então, mantemos assim.

Os dados codificados são armazenados separadamente nos equipamentos. Portanto, o equipamento é como um arquivo SQL com uma instrução create table e insere os registros que usamos. Nós os mantemos pequenos, a menos que haja uma necessidade real de testar com muitos registros.

class ProductModel {
  public function getAllActive() {
    return $this->find('all', array('conditions' => array('active' => 1)));
  }
}

Controlador

O controlador precisa de mais trabalho, porque não queremos testar o modelo com ele. Então, o que fazemos é zombar do modelo. Isso significa: Testamos: método index () que deve retornar uma lista de registros.

Portanto, zombamos do método getAllActive () e adicionamos dados fixos (dois registros, por exemplo). Agora testamos os dados que o controlador envia para a visualização e comparamos se realmente recuperamos esses dois registros.

function testProductIndexLoggedIn() {
  $this->setLoggedIn();
  $this->ProductsController->mock('ProductModel', 'index', function(return array(your records) ));
  $result=$this->ProductsController->index();
  $this->assertEquals(2, count($result['products']));
}

É o bastante. Tentamos adicionar pouca funcionalidade ao controlador, pois isso dificulta o teste. Mas é claro que sempre há algum código nele. Por exemplo, testamos requisitos como: Mostrar esses dois registros apenas se você estiver conectado.

Portanto, o controlador precisa de um mock normalmente e de um pequeno pedaço de dados codificados. Para um sistema de login, talvez outro. Em nosso teste, temos um método auxiliar para isso: setLoggedIn (). Isso simplifica o teste com login ou sem login.

class ProductsController {
  public function index() {
    if($this->loggedIn()) {
      $this->set('products', $this->ProductModel->getAllActive());
    }
  }
}

Visualizações

O teste de visualizações é difícil. Primeiro separamos a lógica que se repete. Colocamos em Helpers e testamos estritamente essas classes. Esperamos sempre a mesma saída. Por exemplo, generateHtmlTableFromArray ().

Depois, temos algumas visualizações específicas do projeto. Nós não testamos isso. Não é realmente desejado testá-los unitariamente. Nós os mantemos para testes de integração. Como colocamos grande parte do código em visualizações, temos um risco menor aqui.

Se você começar a testar aqueles, provavelmente precisará alterar seus testes sempre que alterar um pedaço de HTML que não é útil para a maioria dos projetos.

echo $this->tableHelper->generateHtmlTableFromArray($products);

Teste de integração

Dependendo da sua plataforma, aqui você pode trabalhar com histórias de usuários, etc. Ele pode ser baseado na Web como o Selenium ou outras soluções comparáveis.

Geralmente, apenas carregamos o banco de dados com os equipamentos e afirmamos quais dados devem estar disponíveis. Para testes de integração total, geralmente usamos requisitos muito globais. Portanto: defina o produto como ativo e verifique se o produto fica disponível.

Não testamos tudo novamente, como se os campos corretos estão disponíveis. Testamos os requisitos maiores aqui. Como não queremos duplicar nossos testes do controlador ou da exibição. Se algo realmente for essencial / essencial do seu aplicativo ou por motivos de segurança (verifique a senha NÃO está disponível), nós os adicionamos para garantir que esteja certo.

Os dados codificados são armazenados nos equipamentos.

function testIntegrationProductIndexLoggedIn() {
  $this->setLoggedIn();
  $result=$this->request('products/index');

  $expected='<table';
  $this->assertContains($expected, $result);

  // Some content from the fixture record
  $expected='<td>Product 1 name</td>';
  $this->assertContains($expected, $result);
}
Luc Franken
fonte
Esta é uma ótima resposta, para uma pergunta totalmente diferente.
quer
Obrigado pelo feedback. Você pode estar certo que eu não mencionei isso muito específico. A razão da resposta detalhada é porque vejo uma das coisas mais difíceis ao testar na pergunta. A visão geral de como os testes isolados se ajustam aos diferentes tipos de testes. Por isso, adicionei em todas as partes como os dados são tratados (ou separados). Vou dar uma olhada para ver se consigo esclarecer melhor.
Luc Franken
A resposta foi atualizada com alguns exemplos de código para explicar como testar sem chamar todos os tipos de outras classes.
precisa saber é o seguinte
4

Se você estiver escrevendo testes que envolvem muita DI e fiação, até usar fontes de dados "reais", provavelmente saiu da área de teste de unidade simples e entrou no domínio dos testes de integração.

Para testes de integração, acho que não é uma má idéia ter uma lógica de configuração de dados comum. O principal objetivo desses testes é provar que tudo está configurado corretamente. Isso é bastante independente dos dados concretos enviados pelo seu sistema.

Por outro lado, para testes de unidade, eu recomendaria manter o alvo de uma classe de teste em uma única classe "real" e zombar de todo o resto. Em seguida, você deve realmente codificar os dados de teste para garantir a cobertura do maior número possível de caminhos especiais / de erros anteriores.

Para adicionar um elemento semi-codificado / aleatório aos testes, eu gosto de introduzir fábricas de modelos aleatórios. Em um teste que usa uma instância do meu modelo, eu uso essas fábricas para criar um objeto de modelo válido, mas completamente aleatório, e codifico apenas as propriedades que são de interesse para o teste em questão. Dessa forma, você especifica todos os dados relevantes diretamente em seu teste, poupando também a necessidade de especificar todos os dados irrelevantes e (até certo ponto) testar se não há dependências indesejadas em outros campos do modelo.

Sven Amann
fonte
-1

Eu acho que é bastante comum codificar a maioria dos dados para seus testes.

Considere uma situação simples em que um determinado conjunto de dados causa um erro. Você pode criar especificamente um teste de unidade para esses dados para exercer a correção e garantir que o bug não volte. Com o tempo, seus testes terão um conjunto de dados que abrangem vários casos de teste.

Os dados de teste predefinidos também permitem criar um conjunto de dados que abrange uma ampla e conhecida variedade de situações.

Dito isso, acho que também vale a pena ter alguns dados aleatórios em seus testes.

Sasbury
fonte
Você realmente leu a pergunta e não apenas o título?
Jakob,
valor em ter alguns dados aleatórios em seus testes - Sim, porque não há nada como tentar descobrir o que aconteceu em um teste na única vez em que falha toda semana.
PDR
É importante ter dados aleatórios em seus testes para testes de trote / difusão / entrada. Mas não nos testes de unidade, isso seria um pesadelo.
glenatron