Existe uma maneira mais fácil de testar a validação de argumentos e a inicialização de campo em um objeto imutável?

20

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());
}
Botond Balázs
fonte
3
O título / tag fala sobre o teste (unitário) de valores de campo, o que implica um teste de unidade pós-construção do objeto, mas o trecho de código mostra a validação de argumento / parâmetro de entrada; estes não são os mesmos conceitos.
precisa
2
Para sua informação, você poderia escrever um modelo T4 que facilitaria esse tipo de clichê. (Você pode também considerar string.IsEmpty () além == null.)
Erik Eidt
1
Você já deu uma olhada nos idiomas de autofixture? nuget.org/packages/AutoFixture.Idioms
Esben Skov Pedersen
1
Veja também: stackoverflow.com/questions/291340/…
Doc Brown
1
Você também pode usar o Fody / NullGuard , embora pareça não ter um modo de permissão padrão.
22616 Bob

Respostas:

5

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:

[PreNotNullCheck, PostNotNullCheck]
public string FirstName { get; }

O construtor será este:

public Person(string firstName) {
  // pre not null check
  if (firstName == null) throw new ArgumentNullException(nameof(firstName));

  // assignations + PostConstructor() if needed

  // post not null check
  if (this.FirstName == null) throw new NullReferenceException(nameof(this.FirstName));
}

Dito isto, se você usar as Anotações do JetBrains para verificação nula, também poderá fazer o seguinte:

[JetBrains.Annotations.NotNull, ConstructorParamNotNull]
public string FirstName { get; }

E o construtor será este:

public Person([JetBrains.Annotations.NotNull] string firstName) {
  // pre not null check is implied by ConstructorParamNotNull
  if (firstName == null) throw new ArgumentNullException(nameof(firstName));

  FirstName = firstName;
  // + PostConstructor() if needed

  // post not null check implied by JetBrains.Annotations.NotNull on the property
  if (this.FirstName == null) throw new NullReferenceException(nameof(this.FirstName));
}

Além disso, existem alguns outros recursos além deste.

Javier G.
fonte
Obrigado. Eu apenas tentei e funcionou perfeitamente. Assinalo isso como a resposta aceita, pois aborda todos os problemas da pergunta original.
Botond Balázs
16

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:

internal static T ThrowIfNull<T>(this T o, string paramName) where T : class
{
    if (o == null)
        throw new ArgumentNullException(paramName);

    return o;
}

Você pode então escrever:

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)
    {
        FullName = fullName.ThrowIfNull(nameof(fullName));
        NameAtBirth = nameAtBirth.ThrowIfNull(nameof(nameAtBirth));
        TaxId = taxId.ThrowIfNull(nameof(taxId));
        PhoneNumber = phoneNumber.ThrowIfNull(nameof(fullName));
        Address = address.ThrowIfNull(nameof(address));
    }
}

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.

Robert Harvey
fonte
3
Estranho que isso tenha sido prejudicado. É muito semelhante à abordagem fornecida pela resposta mais votada, exceto pelo fato de não depender de Roslyn.
Robert Harvey
O que eu não gosto nisso é que é vulnerável a modificações no construtor.
precisa
@ jpmc26: O que isso significa?
Robert Harvey
1
Enquanto eu leio sua resposta, está sugerindo que você não teste o construtor , mas crie um método comum chamado em cada parâmetro e teste-o. Se você adicionar um novo par de propriedade / argumento e esquecer de testar nullo argumento no construtor, não obterá um teste com falha.
precisa
@ jpmc26: Naturalmente. Mas isso também é verdade se você escrever da maneira convencional, conforme ilustrado no OP. Tudo o que o método comum faz é mover a parte throwexterna 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.
21716 Robert
7

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:

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)
    {
        FullName = fullName ?? throw new ArgumentNullException(nameof(fullName));
        NameAtBirth = nameAtBirth ?? throw new ArgumentNullException(nameof(nameAtBirth));
        TaxId = taxId ?? throw new ArgumentNullException(nameof(taxId)); ;
        PhoneNumber = phoneNumber ?? throw new ArgumentNullException(nameof(phoneNumber)); ;
        Address = address ?? throw new ArgumentNullException(nameof(address)); ;
    }
}

Você pode experimentar expressões de lançamento por meio do aplicativo da Web Try Roslyn .

David Arno
fonte
Muito mais compacto, obrigado. Metade do número de linhas. Ansioso para C # 7!
Botond Balázs
@ BotondBalázs você pode experimentá-lo na pré-visualização do VS15 agora mesmo, se quiser #
user1306322 16/16
7

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

public Person(
    string fullName,
    string nameAtBirth,
    string taxId,
    PhoneNumber phoneNumber,
    Address address)
{
    FullName = fullName;
    NameAtBirth = nameAtBirth;
    TaxId = taxId;
    PhoneNumber = phoneNumber;
    Address = address;
}

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]atributo

Roman Reiner
fonte
Obrigado, isso parece uma solução geral muito boa. Embora seja muito lamentável que ele modifique o comportamento do idioma por padrão. Eu gostaria muito mais se funcionasse adicionando um [NotNull]atributo em vez de não permitir que nulo fosse o padrão.
Botond Balázs
@ BotondBalázs: O PostSharp possui isso junto com outros atributos de validação de parâmetro. Ao contrário de Fody, não é gratuito. Além disso, o código gerado chamará as DLLs do PostSharp, portanto você precisará incluir as do seu produto. Fody só executará seu código durante a compilação e não será necessário em tempo de execução.
Roman Reiner
@ BotondBalázs: Também achei estranho, a princípio, que o NullGuard se aplica por padrão, mas realmente faz sentido. Em qualquer base de código razoável, permitir nulo deve ser muito menos comum do que procurar nulo. Se você realmente deseja permitir nulo em algum lugar, a abordagem de exclusão o força a ser muito cuidadoso com ela.
Roman Reiner
5
Sim, isso é razoável, mas viola o princípio da menor surpresa . Se um novo programador não souber que o projeto usa o NullGuard, ele terá uma grande surpresa! A propósito, também não entendo o voto negativo, esta é uma resposta muito útil.
Botond Balázs
1
@ BotondBalázs / Roman, parece que o NullGuard tem um novo modo explícito que muda a maneira como funciona por padrão
Kyle W
5

Vale a pena escrever todos esses testes?

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.

Seria uma boa idéia escrever um gerador de código para essas classes, para evitar escrever testes para cada uma delas?

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.

Telastyn
fonte
Obrigado. Talvez eu não era suficiente claro na minha pergunta, mas eu pretendia escrever um gerador que gera classes imutáveis, não testa para eles
Botond Balázs
2
Eu também já vi várias vezes, em vez do que if...throweles terão ThrowHelper.ArgumentNull(myArg, "myArg"), dessa forma a lógica ainda está lá e você não fica enganado na cobertura do código. Onde o ArgumentNullmétodo fará o if...throw.
Mateus
2

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 Namepropriedade 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 ArgumentNullExceptionpodem ser úteis.

Enquanto aguarda o C # 7, você pode usar o método de extensão

public MyClass(string name)
{
    name.ThrowArgumentNullExceptionIfNull(nameof(name));
}

public static void ThrowArgumentNullExceptionIfNull(this object value, string paramName)
{
    if(value == null)
        throw new ArgumentNullException(paramName);
}

Para testar, você pode usar um método de teste parametrizado que usa reflexão para criar nullreferência para cada parâmetro e afirmar a exceção esperada.

Fabio
fonte
1
Esse é um argumento convincente para não testar a inicialização da propriedade.
Botond Balázs
Isso depende de como você está usando o código. Se o código fizer parte de uma biblioteca pública, você nunca deve confiar cegamente em outras pessoas para saber como sua biblioteca (especialmente se elas não tiverem acesso ao código-fonte); no entanto, se é algo que você usa internamente para fazer seu próprio código funcionar, você deve saber como usar seu próprio código, para que as verificações tenham menos finalidade.
EJoshuaS - Restabelece Monica 16/11
2

Você sempre pode escrever um método como o seguinte:

// Just to illustrate how to call this
private static void SomeMethod(string a, string b, string c, string d)
    {
        ValidateArguments(a, b, c, d);
        // ...
    }

    // This is the one to use as a utility function
    private static void ValidateArguments(params object[] args)
    {
        for (int i = 0; i < args.Length; i++)
        {
            if (args[i] == null)
            {
                StackTrace trace = new StackTrace();
                // Get the method that called us
                MethodBase info = trace.GetFrame(1).GetMethod();

                // Get information on the parameter that is null so we can add its name to the exception
                ParameterInfo param = info.GetParameters()[i];

                // Raise the exception on behalf of the caller
                throw new ArgumentNullException(param.Name);
            }
        }
    }

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:

// Note that we already know based on the previous condition that args[i] is not null
else if (args[i].GetType() == typeof(string))
            {
                string argCast = arg as string;

                if (!argCast.Trim().Any())
                {
                    ParameterInfo param = GetParameterInfo(i);

                    throw new ArgumentException(param.Name + " is empty or consists only of whitespace");
                }
            }

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 ):

private static ParameterInfo GetParameterInfo(int index)
    {
        StackTrace trace = new StackTrace();

        // Note that we have to go 2 methods back to get the ValidateArguments method's caller
        MethodBase info = trace.GetFrame(2).GetMethod();

        // Get information on the parameter that is null so we can add its name to the exception
        ParameterInfo param = info.GetParameters()[index];

        return param;
    }
EJoshuaS - Restabelecer Monica
fonte
1
Por que isso está sendo rebaixado? Não é muito ruim
Gaspa79
0

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:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

oopexpert
fonte
1
Discordo respeitosamente do seu último ponto. Não ter que digitar o mesmo clichê para a 100th vez que ajuda a reduzir a chance de cometer um erro. Imagine se fosse Java, não C #. Eu teria que definir um campo de apoio e um método getter para cada propriedade. O código se tornaria completamente ilegível e o que é importante seria obscurecido pela infraestrutura desajeitada.
Botond Balázs