TDD com padrão de repositório

10

No meu novo projeto, decidi tentar com o TDD. E, no começo, encontrei um problema. A primeira coisa que quero fazer no meu aplicativo é oferecer a capacidade de ler dados da fonte de dados. Para esse propósito, quero usar o padrão de repositório. E agora:

  • Se test for para implementação real da interface do repositório, testarei a classe que tem acesso ao banco de dados e sei que devo evitar isso.
  • Se teste não for uma implementação real do padrão do repositório, estarei testando bem ... apenas zombe. Não haverá nenhum código de produção testado nesses testes de unidade.

Estou pensando nisso há dois dias e ainda não consigo encontrar uma solução razoável. O que eu deveria fazer?

Thaven
fonte

Respostas:

11

O que um repositório faz é converter do seu domínio para a estrutura DAL, como NHibernate ou Doctrine, ou suas classes de execução SQL. Isso significa que seu repositório chamará métodos na referida estrutura para desempenhar suas funções: seu repositório constrói as consultas necessárias para buscar os dados. Se você não estiver usando uma estrutura ORM (espero que você esteja ...), o repositório seria o local onde as instruções SQL brutas são construídas.

O mais básico desses métodos é o save: na maioria dos casos, isso simplesmente passa o objeto do repositório para a unidade de trabalho (ou a sessão).

public void Save(Car car)
{
    session.Save(car);
}

Mas vejamos outro exemplo, por exemplo, buscando um carro pelo seu ID. Pode parecer

public function GetCarWithId(String id)
{
    return Session.QueryOver<Car>()
                    .Where(x => x.Id == id)
                    .SingleOrDefault();
}

Ainda não é muito complexo, mas você pode imaginar com várias condições (consiga todos os carros fabricados após 2010 para todas as marcas do grupo 'Volkswagen') isso fica complicado. Então, na verdadeira moda TDD, você precisa testar isso. Existem várias maneiras de fazer isso.

Opção 1: zombe das chamadas feitas para a estrutura ORM

Claro, você pode zombar do objeto Session e simplesmente afirmar que as chamadas corretas são feitas. Enquanto isso testa o repositório, ele não é realmente orientado a testes , porque você está apenas testando se o repositório parece internamente da maneira que deseja. O teste basicamente diz 'o código deve ficar assim'. Ainda assim, é uma abordagem válida, mas parece que esse tipo de teste tem muito pouco valor.

Opção 2: (Re) construa o banco de dados a partir dos testes

Algumas estruturas DAL permitem criar a estrutura completa do banco de dados com base nos arquivos de mapeamento criados para mapear o domínio nas tabelas. Para essas estruturas, a maneira de testar repositórios geralmente é criar o banco de dados com um banco de dados na memória na primeira etapa do teste e adicionar objetos usando a estrutura DAL ao banco de dados na memória. Depois disso, você pode usar o repositório no banco de dados na memória para testar se os métodos funcionam. Esses testes são mais lentos, mas muito válidos e conduzem seus testes. Requer alguma cooperação da sua estrutura DAL.

Opção 3: teste em um banco de dados real

Outra abordagem é testar em um banco de dados real e isolar o mais unittest. Você pode fazer isso de várias maneiras: envolva seus testes com uma transação, limpe manualmente (não seria muito difícil de manter), reconstrua completamente o banco de dados após cada etapa ... Dependendo do aplicativo que você está construindo, isso pode ou não não ser viável. Em meus aplicativos, posso criar completamente um banco de dados de desenvolvimento local a partir do controle de origem e minhas unidades nos repositórios usam transações para isolar completamente os testes um do outro (transação aberta, inserir dados, repositório de teste, transação de reversão). Cada construção primeiro configura o banco de dados de desenvolvimento local e, em seguida, executa unittests isolados de transação para os repositórios nesse banco de dados de desenvolvimento local. Isto'

Não teste o DAL

Se você estiver usando uma estrutura DAL como o NHibernate, evite a necessidade de testar essa estrutura. Você pode testar seus arquivos de mapeamento salvando, recuperando e comparando um objeto de domínio para garantir que tudo está bem (desative qualquer tipo de cache), mas não é tão necessário quanto muitos outros testes que você deve escrever. Costumo fazer isso principalmente para coleções de pais com condições para os filhos.

Ao testar o retorno de seus repositórios, você pode simplesmente verificar se alguma propriedade de identificação em seu objeto de domínio corresponde. Isso pode ser um ID, mas em testes geralmente é mais benéfico verificar uma propriedade legível por humanos. No 'me consiga todos os carros fabricados depois de 2010 ...', isso poderia simplesmente verificar se cinco carros foram devolvidos e as placas estão 'inserir lista aqui'. O benefício adicional é que obriga a pensar na classificação E seu teste obriga automaticamente a classificação. Você ficaria surpreso com o número de aplicativos que ordenam várias vezes (retorno ordenado do banco de dados, antes de criar um objeto de exibição e depois classifica o objeto de exibição, todos na mesma propriedade, apenas no caso ) ou assume implicitamente as classificações do repositório e remove acidentalmente que estavam no caminho, interrompendo a interface do usuário.

'Teste de unidade' é apenas um nome

Na minha opinião, testes de unidade deve principalmente não bater o banco de dados. Você constrói um aplicativo para que cada pedaço de código que precise de dados de uma fonte faça isso com um repositório, e esse repositório seja injetado como uma dependência. Isso permite zombaria fácil e toda a qualidade do TDD que você deseja. Mas, no final, você deseja garantir que seus repositórios cumpram suas funções e se a maneira mais fácil de fazer isso é acessar um banco de dados, bem, que assim seja. Há muito tempo deixei de lado a noção de que 'testes de unidade não devem tocar o banco de dados' e aprendi que existem razões muito reais para fazer isso. Mas somente se você puder fazer isso automaticamente e repetidamente. E o tempo que chamamos de teste como "teste de unidade" ou "teste de integração" é discutível.

JDT
fonte
3
Testes de unidade e testes de integração têm finalidades diferentes. Os nomes para esses testes não são meramente decorativos; eles também são descritivos.
Robert Harvey
9
  1. Não teste métodos de repositório triviais ou óbvios.

    Se os métodos são operações CRUD triviais, tudo o que você realmente está testando é se os parâmetros estão mapeados corretamente. Se você tiver testes de integração, esses erros se tornarão imediatamente aparentes.

    Este é o mesmo princípio que se aplica a propriedades triviais, como esta:

    public property SomeProperty
    {
        get { return _someProperty; }
        set { _someProperty = value; }
    }
    

    Você não testa, porque não há nada para testar. Não há validação ou outra lógica na propriedade que precise ser verificada.

  2. Se você ainda deseja testar esses métodos ...

    Zombarias são a maneira de fazê-lo. Lembre-se, estes são testes de unidade. Você não testa o banco de dados com testes de unidade; é para isso que servem os testes de integração.

Mais informações
The Full Stack, Parte 3: Criando um repositório usando TDD (comece a assistir em cerca de 16 minutos).

Robert Harvey
fonte
3
Claro, eu entendo isso. Ainda assim, se for uma abordagem TDD, não devo escrever nenhum código se não tiver testes para esse código primeiro, certo?
Thaven
1
@ Thaven - há uma série de vídeos no youtube intitulados "is tdd dead?". Assista-os. Eles abordam muitos pontos interessantes, um dos quais é a noção de que aplicar TDD em todos os níveis do seu aplicativo não é necessariamente a melhor idéia. "nenhum código sem um teste reprovado" é uma posição muito extrema, é uma das conclusões.
Jules
2
@Jules: Qual é o tl; dw?
Robert Harvey
1
@RobertHarvey É difícil resumir, mas o ponto mais importante foi que tratar o TDD como uma religião que sempre deve ser observada é um erro. A escolha de usá-lo faz parte de um compromisso e é necessário considerar que (1) você pode trabalhar mais rápido sem problemas em alguns problemas e (2) pode levá-lo a uma solução mais complexa do que precisa, particularmente se você estiver usando muitas zombarias.
Jules
1
+1 para o ponto 1. Os testes podem estar errados, mas geralmente são triviais. Não faz sentido testar uma função cuja correção é mais óbvia que a do teste. Não é como se obter 100% de cobertura de código o levasse a qualquer lugar perto de testar todas as execuções possíveis do programa; assim, você também pode ser esperto sobre onde gasta o esforço de teste.
Doval