Meu domínio consiste em muitas classes simples e imutáveis como esta:
public class Person
{
public string FullName { get; }
public string NameAtBirth { get; }
public string TaxId { get; }
public PhoneNumber PhoneNumber { get; }
public Address Address { get; }
public Person(
string fullName,
string nameAtBirth,
string taxId,
PhoneNumber phoneNumber,
Address address)
{
if (fullName == null)
throw new ArgumentNullException(nameof(fullName));
if (nameAtBirth == null)
throw new ArgumentNullException(nameof(nameAtBirth));
if (taxId == null)
throw new ArgumentNullException(nameof(taxId));
if (phoneNumber == null)
throw new ArgumentNullException(nameof(phoneNumber));
if (address == null)
throw new ArgumentNullException(nameof(address));
FullName = fullName;
NameAtBirth = nameAtBirth;
TaxId = taxId;
PhoneNumber = phoneNumber;
Address = address;
}
}
Escrever as verificações nulas e a inicialização da propriedade já está ficando muito entediante, mas atualmente escrevo testes de unidade para cada uma dessas classes para verificar se a validação de argumentos funciona corretamente e se todas as propriedades foram inicializadas. Parece um trabalho extremamente chato, com benefícios incomensuráveis.
A solução real seria o C # suportar nativamente tipos de referência imutáveis e não anuláveis. Mas o que posso fazer para melhorar a situação nesse meio tempo? Vale a pena escrever todos esses testes? Seria uma boa idéia escrever um gerador de código para essas classes, para evitar escrever testes para cada uma delas?
Aqui está o que eu tenho agora com base nas respostas.
Eu poderia simplificar as verificações nulas e a inicialização da propriedade para ficar assim:
FullName = fullName.ThrowIfNull(nameof(fullName));
NameAtBirth = nameAtBirth.ThrowIfNull(nameof(nameAtBirth));
TaxId = taxId.ThrowIfNull(nameof(taxId));
PhoneNumber = phoneNumber.ThrowIfNull(nameof(phoneNumber));
Address = address.ThrowIfNull(nameof(address));
Usando a seguinte implementação por Robert Harvey :
public static class ArgumentValidationExtensions
{
public static T ThrowIfNull<T>(this T o, string paramName) where T : class
{
if (o == null)
throw new ArgumentNullException(paramName);
return o;
}
}
É fácil testar as verificações nulas usando o comandoGuardClauseAssertion
from AutoFixture.Idioms
(obrigado pela sugestão, Esben Skov Pedersen ):
var fixture = new Fixture().Customize(new AutoMoqCustomization());
var assertion = new GuardClauseAssertion(fixture);
assertion.Verify(typeof(Address).GetConstructors());
Isso pode ser compactado ainda mais:
typeof(Address).ShouldNotAcceptNullConstructorArguments();
Usando este método de extensão:
public static void ShouldNotAcceptNullConstructorArguments(this Type type)
{
var fixture = new Fixture().Customize(new AutoMoqCustomization());
var assertion = new GuardClauseAssertion(fixture);
assertion.Verify(type.GetConstructors());
}
fonte
Respostas:
Eu criei um modelo t4 exatamente para esse tipo de casos. Para evitar escrever muitos clichês para as classes imutáveis.
https://github.com/xaviergonz/T4Immutable T4Immutable é um modelo T4 para aplicativos C # .NET que gera código para classes imutáveis.
Falando especificamente sobre testes não nulos, se você usar isso:
O construtor será este:
Dito isto, se você usar as Anotações do JetBrains para verificação nula, também poderá fazer o seguinte:
E o construtor será este:
Além disso, existem alguns outros recursos além deste.
fonte
Você pode obter algumas melhorias com uma refatoração simples que pode facilitar o problema de escrever todas essas cercas. Primeiro, você precisa deste método de extensão:
Você pode então escrever:
Retornar o parâmetro original no método de extensão cria uma interface fluente, para que você possa estender esse conceito com outros métodos de extensão, se desejar, e encadeá-los todos juntos em sua atribuição.
Outras técnicas são mais elegantes no conceito, mas progressivamente mais complexas na execução, como decorar o parâmetro com um
[NotNull]
atributo e usar o Reflection como este .Dito isso, você pode não precisar de todos esses testes, a menos que sua classe faça parte de uma API pública.
fonte
null
o argumento no construtor, não obterá um teste com falha.throw
externa do construtor; você não está realmente transferindo o controle dentro do construtor para outro lugar. É apenas um método utilitário e uma maneira de fazer as coisas fluentemente , se você escolher.No curto prazo, não há muito que você possa fazer sobre o tédio de escrever esses testes. No entanto, existe alguma ajuda com expressões throw que devem ser implementadas como parte da próxima versão do C # (v7), provavelmente nos próximos meses:
Você pode experimentar expressões de lançamento por meio do aplicativo da Web Try Roslyn .
fonte
Estou surpreso que ninguém tenha mencionado o NullGuard . Está disponível via NuGet e automaticamente as verificações nulas na IL durante o tempo de compilação.
Portanto, seu código construtor seria simplesmente
e o NullGuard adicionará essas verificações nulas para você transformá-lo exatamente no que você escreveu.
Observe, porém, que o NullGuard é opt-out , ou seja, ele adiciona essas verificações nulas a todos os argumentos de método e construtor, getter e setter de propriedades e até verifica os valores de retorno do método, a menos que você permita explicitamente um valor nulo com o
[AllowNull]
atributofonte
[NotNull]
atributo em vez de não permitir que nulo fosse o padrão.Não, provavelmente não. Qual é a probabilidade de você estragar tudo? Qual é a probabilidade de que alguma semântica mude de baixo de você? Qual é o impacto se alguém estragar tudo?
Se você está gastando muito tempo fazendo testes para algo que raramente falha, e é uma correção trivial se isso acontecer ... talvez não valha a pena.
Talvez? Esse tipo de coisa poderia ser feito facilmente com reflexão. Algo a considerar é a geração de código para o código real, para que você não tenha N classes que possam ter erro humano. Prevenção de bugs> detecção de bugs.
fonte
if...throw
eles terãoThrowHelper.ArgumentNull(myArg, "myArg")
, dessa forma a lógica ainda está lá e você não fica enganado na cobertura do código. Onde oArgumentNull
método fará oif...throw
.Vale a pena escrever todos esses testes?
Não.
Porque tenho certeza de que você testou essas propriedades por meio de outros testes de lógica em que essas classes são usadas.
Por exemplo, você pode ter testes para a classe Factory que possuem asserções com base nessas propriedades (instância criada com a
Name
propriedade atribuída corretamente, por exemplo).Se essas classes forem expostas à API pública usada por algum terceiro / usuário final (@EJoshua, obrigado por perceber), os testes para o esperado
ArgumentNullException
podem ser úteis.Enquanto aguarda o C # 7, você pode usar o método de extensão
Para testar, você pode usar um método de teste parametrizado que usa reflexão para criar
null
referência para cada parâmetro e afirmar a exceção esperada.fonte
Você sempre pode escrever um método como o seguinte:
No mínimo, você economizará digitação se você tiver vários métodos que exigem esse tipo de validação. Obviamente, esta solução pressupõe que nenhum dos parâmetros do seu método possa ser
null
, mas você pode modificá-lo para alterar isso, se desejar.Você também pode estender isso para executar outra validação específica de tipo. Por exemplo, se você tiver uma regra de que as strings não podem ser meramente espaços em branco ou vazios, adicione a seguinte condição:
O método GetParameterInfo a que me refiro basicamente faz a reflexão (para que eu não precise continuar digitando a mesma coisa repetidamente, o que seria uma violação do princípio DRY ):
fonte
Eu não sugiro lançar exceções do construtor. O problema é que você terá dificuldade em testar isso, pois precisa passar parâmetros válidos, mesmo que sejam irrelevantes para o seu teste.
Por exemplo: Se você deseja testar se o terceiro parâmetro lança uma exceção, é necessário passar o primeiro e o segundo corretamente. Portanto, seu teste não está mais isolado. quando as restrições do primeiro e do segundo parâmetro mudam, este caso de teste falha, mesmo que teste o terceiro parâmetro.
Sugiro usar a API de validação java e externalizar o processo de validação. Sugiro ter quatro responsabilidades (classes) envolvidas:
Um objeto de sugestão com parâmetros anotados de validação Java com um método validate que retorna um Set of ConstraintViolations. Uma vantagem é que você pode repassar esses objetos sem que a asserção seja válida. Você pode adiar a validação até que seja necessário sem tentar instanciar o objeto de domínio. O objeto de validação pode ser usado em diferentes camadas, pois é um POJO sem conhecimento específico da camada. Pode fazer parte da sua API pública.
Uma fábrica para o objeto de domínio responsável pela criação de objetos válidos. Você passa o objeto de sugestão e a fábrica chama a validação e cria o objeto de domínio se tudo estiver bem.
O próprio objeto de domínio que deve estar inacessível para instanciação para outros desenvolvedores. Para fins de teste, sugiro ter essa classe no escopo do pacote.
Uma interface de domínio público para ocultar o objeto de domínio concreto e dificultar o uso indevido.
Eu não sugiro verificar se há valores nulos. Assim que você entra na camada de domínio, você se livra de passar nulo ou retornar nulo, desde que não tenha cadeias de objetos com finalidades ou funções de pesquisa para objetos únicos.
Um outro ponto: escrever o mínimo de código possível não é uma métrica para a qualidade do código. Pense em competições de jogos de 32k. Esse código é o código mais compacto, mas também o mais confuso que você pode ter, porque se preocupa apenas com problemas técnicos e semântica. Mas semântica são os pontos que tornam as coisas abrangentes.
fonte