Não me considero um especialista em DDD, mas, como arquiteto de soluções, tento aplicar as melhores práticas sempre que possível. Eu sei que há muita discussão em torno dos prós e contras do "estilo" de setter (público) no DDD e posso ver os dois lados do argumento. Meu problema é que trabalho em uma equipe com uma ampla diversidade de habilidades, conhecimentos e experiência, o que significa que não posso confiar que todo desenvolvedor fará as coisas da maneira "certa". Por exemplo, se nossos objetos de domínio forem projetados para que as alterações no estado interno do objeto sejam executadas por um método, mas forneçam configuradores de propriedade pública, alguém inevitavelmente definirá a propriedade em vez de chamar o método. Use este exemplo:
public class MyClass
{
public Boolean IsPublished
{
get { return PublishDate != null; }
}
public DateTime? PublishDate { get; set; }
public void Publish()
{
if (IsPublished)
throw new InvalidOperationException("Already published.");
PublishDate = DateTime.Today;
Raise(new PublishedEvent());
}
}
Minha solução foi tornar privados os setters de propriedades, o que é possível porque o ORM que estamos usando para hidratar os objetos usa reflexão, para que ele possa acessar os setters privados. No entanto, isso apresenta um problema ao tentar escrever testes de unidade. Por exemplo, quando eu quero escrever um teste de unidade que verifique o requisito de que não podemos republicar, preciso indicar que o objeto já foi publicado. Certamente, posso fazer isso chamando Publish duas vezes, mas meu teste pressupõe que o Publish foi implementado corretamente na primeira chamada. Isso parece um pouco fedido.
Vamos tornar o cenário um pouco mais real com o seguinte código:
public class Document
{
public Document(String title)
{
if (String.IsNullOrWhiteSpace(title))
throw new ArgumentException("title");
Title = title;
}
public String ApprovedBy { get; private set; }
public DateTime? ApprovedOn { get; private set; }
public Boolean IsApproved { get; private set; }
public Boolean IsPublished { get; private set; }
public String PublishedBy { get; private set; }
public DateTime? PublishedOn { get; private set; }
public String Title { get; private set; }
public void Approve(String by)
{
if (IsApproved)
throw new InvalidOperationException("Already approved.");
ApprovedBy = by;
ApprovedOn = DateTime.Today;
IsApproved = true;
Raise(new ApprovedEvent(Title));
}
public void Publish(String by)
{
if (IsPublished)
throw new InvalidOperationException("Already published.");
if (!IsApproved)
throw new InvalidOperationException("Cannot publish until approved.");
PublishedBy = by;
PublishedOn = DateTime.Today;
IsPublished = true;
Raise(new PublishedEvent(Title));
}
}
Quero escrever testes de unidade que verifiquem:
- Não posso publicar a menos que o Documento tenha sido aprovado
- Não consigo republicar um documento
- Quando publicados, os valores PublishedBy e PublishedOn são definidos corretamente
- Quando publicado, o PublishedEvent é gerado
Sem acesso aos setters, não consigo colocar o objeto no estado necessário para executar os testes. A abertura do acesso aos levantadores anula o objetivo de impedir o acesso.
Como você resolveu esse problema?
fonte
Respostas:
Se você não pode colocar o objeto no estado necessário para executar um teste, não pode colocar o objeto no estado no código de produção, portanto, não há necessidade de testar esse estado. Obviamente, isso não é verdade no seu caso, você pode colocar seu objeto no estado necessário, basta chamar Aprovar.
Não posso publicar a menos que o Documento tenha sido aprovado: escreva um teste que chamar publicar antes de chamar aprovar causa o erro correto sem alterar o estado do objeto.
Não consigo republicar um documento: escreva um teste que aprove um objeto e chame a publicação de uma vez com êxito, mas a segunda vez causa o erro correto sem alterar o estado do objeto.
Quando publicados, os valores PublishedBy e PublishedOn são definidos corretamente: escreva um teste que chame aprovar e depois publique, afirme que o estado do objeto muda corretamente
Quando publicado, o PublishedEvent é gerado: conecte-se ao sistema de eventos e defina um sinalizador para garantir que ele seja chamado
Você também precisa escrever um teste para aprovar.
Em outras palavras, não teste a relação entre campos internos e IsPublished e IsApproved, seu teste será bastante frágil se você fizer isso, pois alterar seu campo significaria alterar seu código de testes, portanto o teste seria inútil. Em vez disso, você deve testar o relacionamento entre chamadas de métodos públicos, dessa maneira, mesmo se você modificar os campos, não precisará modificar o teste.
fonte
setup()
método - não no teste em si.approve()
alguma forma frágil, mas depende desetApproved(true)
alguma forma não?approve()
é uma dependência legítima nos testes porque é uma dependência nos requisitos. Se a dependência existisse apenas nos testes, isso seria outro problema.push()
epop()
independentemente?Ainda outra abordagem é criar um construtor da classe que permita que as propriedades internas sejam definidas na instanciação:
fonte
Uma estratégia é que você herde a classe (neste caso, Documento) e escreva testes na classe herdada. A classe herdada permite definir o estado do objeto nos testes.
No C #, uma estratégia poderia ser tornar os setters internos e expor os internos para testar o projeto.
Você também pode usar a API da classe como você descreveu ("Eu certamente posso fazer isso chamando de Publicar duas vezes"). Isso definiria o estado do objeto usando os serviços públicos do objeto, não me parece muito fedorento. No caso do seu exemplo, provavelmente seria assim que eu faria.
fonte
Para testar em isolamento absoluto os comandos e as consultas que os objetos de domínio recebem, estou acostumado a fornecer a cada teste uma serialização do objeto no estado esperado. Na seção de organização do teste, ele carrega o objeto para testar a partir de um arquivo que eu preparei anteriormente. No começo, comecei com serializações binárias, mas o json provou ser muito mais fácil de manter. Isso provou funcionar bem, sempre que o isolamento absoluto nos testes fornece valor real.
edite apenas uma observação, algumas vezes a serialização JSON falha (como no caso dos gráficos de objetos cíclicos, que são cheiros). Em tais situações, resgato a serialização binária. É um pouco pragmático, mas funciona. :-)
fonte
Você diz
e
e tenho que pensar que usar a reflexão para ignorar os controles de acesso em suas aulas não é o que eu descreveria como "melhor prática". Vai ser terrivelmente lento também.
Pessoalmente, eu descartaria sua estrutura de teste de unidade e aceitaria algo da classe - parece que você está escrevendo testes do ponto de vista de testar toda a classe, o que é bom. No passado, para alguns componentes complicados que precisavam de testes, eu incorporamos os códigos de assertivos e de configuração na própria classe (costumava ser um padrão de design comum ter um método test () em todas as classes), então você cria um cliente que simplesmente instancia um objeto e chama o método de teste, que pode ser configurado como você desejar, sem ser desagradável, como hacks de reflexão.
Se você estiver preocupado com o inchaço do código, basta agrupar os métodos de teste no #ifdefs para torná-los disponíveis apenas no código de depuração (provavelmente a melhor prática em si)
fonte