Teste de unidade para testar a criação de um objeto de domínio

11

Eu tenho um teste de unidade, que se parece com isso:

[Test]
public void Should_create_person()
{
     Assert.DoesNotThrow(() => new Person(Guid.NewGuid(), new DateTime(1972, 01, 01));
}

Estou afirmando que um objeto Pessoa é criado aqui, ou seja, que a validação não falha. Por exemplo, se o Guid for nulo ou a data de nascimento for anterior a 01/01/1900, a validação falhará e uma exceção será lançada (significando que o teste falhará).

O construtor fica assim:

public Person(Id id, DateTime dateOfBirth) :
        base(id)
    {
        if (dateOfBirth == null)
            throw new ArgumentNullException("Date of Birth");
        elseif (dateOfBith < new DateTime(1900,01,01)
            throw new ArgumentException("Date of Birth");
        DateOfBirth = dateOfBirth;
    }

É uma boa ideia para um teste?

Nota : Estou seguindo uma abordagem classicista para testar a unidade do modelo de domínio, se isso tiver alguma influência.

w0051977
fonte
O construtor tem alguma lógica que vale a pena ser afirmada após a inicialização?
LAIV
2
Nunca se preocupe em testar construtores !!! A construção deve seguir em frente. Você espera falhas em Guid.NewGuid () ou construtor de DateTime?
precisa saber é o seguinte
@ Laiv, consulte a atualização da pergunta.
w0051977
1
Não vale a pena implementar um teste como o que você compartilhou. No entanto, eu testaria também o oposto. Eu testaria o caso em que birthDate causa um erro. Esse é o invariante da classe que você deseja que esteja sob controle e teste.
LAIV
3
O teste parece bom, exceto por uma coisa: o nome. Should_create_person? O que deve criar uma pessoa? Dê a ele um nome significativo, como Creating_person_with_valid_data_succeeds.
David Arno

Respostas:

18

Esse é um teste válido (embora exagerado) e às vezes faço isso para testar a lógica do construtor, no entanto, como Laiv mencionou nos comentários, você deve se perguntar por quê.

Se o seu construtor estiver assim:

public Person(Guid guid, DateTime dob)
{
  this.Guid = guid;
  this.Dob = dob;
}

Há muito sentido em testar se lança? Se os parâmetros foram atribuídos corretamente, eu entendo, mas seu teste é um exagero.

No entanto, se o seu teste fizer algo assim:

public Person(Guid guid, DateTime dob)
{
  if(guid == default(Guid)) throw new ArgumentException("Guid is invalid");
  if(dob == default(DateTime)) throw new ArgumentException("Dob is invalid");

  this.Guid = guid;
  this.Dob = dob;
}

Em seguida, seu teste se torna mais relevante (quando você está lançando exceções em algum lugar do código).

Uma coisa que eu diria, geralmente é uma prática ruim ter muita lógica em seu construtor. A validação básica (como as verificações nulas / padrão que estou fazendo acima) está ok. Mas se você está se conectando a bancos de dados e carregando dados de alguém, é aí que o código começa a cheirar muito ...

Por esse motivo, se vale a pena testar seu construtor (porque há muita lógica em andamento), talvez algo esteja errado.

Você certamente terá outros testes cobrindo essa classe em camadas de lógica de negócios, construtores e atribuições de variáveis ​​certamente obterão cobertura completa desses testes. Portanto, talvez não faça sentido adicionar testes específicos especificamente para o construtor. No entanto, nada é preto e branco e eu não teria nada contra esses testes se estivesse analisando o código - mas questionaria se eles agregam muito valor acima e além dos testes em outras partes da sua solução.

No seu exemplo:

public Person(Id id, DateTime dateOfBirth) :
        base(id)
    {
        if (dateOfBirth == null)
            throw new ArgumentNullException("Date of Birth");
        elseif (dateOfBith < new DateTime(1900,01,01)
            throw new ArgumentException("Date of Birth");
        DateOfBirth = dateOfBirth;
    }

Você não está apenas validando, mas também está chamando um construtor de base. Para mim, isso fornece mais motivos para esses testes, pois eles agora têm a lógica de construtor / validação dividida em duas classes, o que diminui a visibilidade e aumenta o risco de alterações inesperadas.

TLDR

Há algum valor para esses testes, no entanto, a lógica de validação / atribuição provavelmente será coberta por outros testes em sua solução. Se há muita lógica nesses construtores que requer testes significativos, isso sugere para mim que há um cheiro desagradável de código à espreita.

Liath
fonte
@Laith, consulte a atualização para a minha pergunta
w0051977
Percebo que você está chamando um construtor de base no seu exemplo. IMHO isso agrega mais valor ao seu teste, a lógica do construtor agora está dividida em duas classes e, portanto, apresenta um risco ligeiramente maior de alteração, dando mais motivos para testá-lo.
Liath 29/01
"No entanto, se o seu teste fizer algo assim:" <Você não quer dizer "se o seu construtor fizer algo assim" ?
Kodo Johnson
"Existe algum valor para esses testes" - de maneira interessante para mim, o valor está mostrando que poderíamos tornar esse teste redundante usando uma nova classe para representar o dob da pessoa (por exemplo PersonBirthdate) que executa a validação da data de nascimento. Da mesma forma, a Guidverificação pode ser implementada na Idclasse. Isso significa que você realmente não precisa mais ter essa lógica de validação no Personconstrutor, pois não é possível construir uma com dados inválidos - exceto nullrefs. Claro, você tem que escrever testes para as outras duas classes :)
Stephen Byrne
12

Já é uma boa resposta aqui, mas acho que vale a pena mencionar uma coisa adicional.

Ao fazer TDD "pelo livro", é preciso escrever um teste primeiro que chame o construtor, mesmo antes de o construtor ser implementado. Esse teste pode realmente parecer com o que você apresentou, mesmo se houvesse uma lógica de validação zero na implementação do construtor.

Observe também que, para TDD, deve-se escrever outro teste primeiro, como

  Assert.Throws<ArgumentException>(() => new Person(Guid.NewGuid(), 
        new DateTime(1572, 01, 01));

antes de adicionar a verificação para DateTime(1900,01,01)o construtor.

No contexto TDD, o teste mostrado faz todo sentido.

Doc Brown
fonte
Bom ângulo que eu não tinha considerado!
Liath 29/01
1
Isso me demonstra por que uma forma tão rígida de TDD é uma perda de tempo: o teste deve ter valor após a escrita do código ou você está escrevendo cada linha de código duas vezes, uma como afirmação e outra como código. Eu argumentaria que o próprio construtor não é um pedaço de lógica que precisa ser testado; a regra de negócios "as pessoas nascidas antes de 1900 não devem ser representáveis" é testável, e o construtor é onde essa regra é implementada, mas quando o teste de um construtor vazio adicionaria valor ao projeto?
IMSoP 30/01
É realmente tdd pelo livro? Eu criaria instância e chamaria seu método imediatamente em um código. Então, eu escreveria o teste para esse método e, ao fazer isso, também precisaria criar uma instância para esse método, para que o construtor e o método sejam abordados nesse teste. A menos que no construtor exista alguma lógica, mas essa parte é coberta por Liath.
Rafał Łużyński 30/01
@ RafałŁużyński: TDD "pelo livro" é sobre escrever testes primeiro . Na verdade, significa sempre escrever um teste com falha primeiro (não compilar contagens como falha também). Então você primeiro escreve um teste chamando o construtor mesmo quando não há construtor . Então você tenta compilar (que falha), depois implementa um construtor vazio, compila, executa o teste, resultado = verde. Em seguida, você escreve o primeiro teste que falhou e o executa - result = red, depois adiciona a funcionalidade para tornar o teste "verde" novamente e assim por diante.
Doc Brown
Claro. Não quis dizer que escrevi a implementação primeiro e depois o teste. Eu apenas escrevo "uso" desse código em um nível acima, testo esse código e o implemento. Eu estou fazendo "Fora TDD" normalmente.
Rafał Łużyński 30/01