Autofixture: Solicitação: Adicionar Método Clone ao IFixture

Criado em 24 set. 2020  ·  9Comentários  ·  Fonte: AutoFixture/AutoFixture

Introdução

Eu gostaria que IFixture tivesse um método Clone ou algo semelhante para criar uma cópia do fixture.

No momento, posso criar um método de extensão para fazer isso:

public static IFixture Clone( this IFixture fixture)
{
   var cloneFixture = new Fixture();

   cloneFixture.Behaviors.Clear();
   foreach ( var behavior in fixture.Behaviors)
   {
      cloneFixture.Behaviors.Add( behavior );
   }

   cloneFixture.Customizations.Clear();
   foreach ( var customization in fixture.Customizations )
   {
      cloneFixture.Customizations.Add( customization );
   }

   cloneFixture.OmitAutoProperties = fixture.OmitAutoProperties;
   cloneFixture.RepeatCount = fixture.RepeatCount;

   cloneFixture.ResidueCollectors.Clear();
   foreach ( var residueCollector in fixture.ResidueCollectors )
   {
      cloneFixture.ResidueCollectors.Add( residueCollector );
   }

   return cloneFixture;
}

Mas acho que essa capacidade de fazer uma cópia deve estar no próprio projeto.

Detalhes

Meu cenário é construir algumas instâncias de uma classe com muitos parâmetros em um teste. Além disso, quero que um parâmetro de construtor seja um determinado valor para uma instância e um valor de construtor diferente para uma instância diferente. Existem várias maneiras de fazer isso; a maneira que tenho feito é:

fixture.Customizations.Add( new FilteringSpecimenBuilder(
            new FixedBuilder( mediaId ),
            new ParameterSpecification( 
                typeof( ObjectId ), 
                "mediaId" ) ) );
var mediaVM = fixture.Build<MediaViewModel>()   
                     .With( mvm => mvm.ParentMixerId, mixerId )
                     .With( mvm => mvm.TrackId, trackId )
                     .Create();

_ = fixture.Customizations.Remove( fixture.Customizations.Last() );

//...

A razão para a remoção da personalização é que tentei sem pensar que talvez a última personalização adicionada tenha uma precedência maior e seja usada; mas não foi o caso.

Se eu tentar simplificar isso com algum tipo de método de extensão assim:

public static IFixture AddCustomization<T>( this IFixture fixture, T value, string name )
{
   fixture.Customizations.Add( new FilteringSpecimenBuilder(
         new FixedBuilder( value ),
         new ParameterSpecification(
            typeof( T ),
            name ) ) );
   return fixture;
}

Pode funcionar em alguns lugares, mas não em outros, porque depois que o objeto é construído, o acessório contém uma personalização que não desejo mais e preciso remover.

Eu preferiria não ter uma linha para remover as personalizações adicionadas. Portanto, se, em vez disso, meu método de extensão fez uma cópia do meu acessório ao qual adiciono modificações à cópia, minhas criações funcionam conforme o esperado e o acessório original permanece intocado.

Também funcionaria é a capacidade de adicionar uma personalização que é removida automaticamente após a criação do objeto.

Espero que tudo isso faça sentido.

Obrigado por considerar meu problema: smiley:

feature request

Comentários muito úteis

@ajorians Eu estava prestes a encerrar o problema devido à inatividade, mas não consegui passar por aquele construtor gigantesco.
Tenho que perguntar, você considerou mudar seu design?

Olhando para o seu construtor, posso ver vários problemas e soluções potenciais para o seu problema.

Em primeiro lugar, você está misturando os conceitos de entidades novas e injetáveis.
A solução mais direta é mover suas dependências injetáveis ​​(também conhecidas como interfaces de serviço) para propriedades e injetá-las usando injeção de propriedade.
Isso resolverá seu problema imediato de ter que copiar Fixture e tornará o construtor mais gerenciável.

O segundo é lidar com o cheiro de código de injeção excessiva .
Isso significa mover os serviços mais comumente usados ​​juntos para abstrações separadas.
Isso deve ajudar seu projeto a respeitar o SRP e, por extensão, deve reduzir a enorme quantidade de configuração que você precisa fazer no momento, em seus testes.

Como a classe se chama ViewModel , presumo que você tenha um aplicativo MVVM. É provável que você possa desacoplar ainda mais seu aplicativo, introduzindo um modelo de sistema de mensagens. Talvez um agregador de eventos. A maioria dos frameworks MVVM os tem embutidos, então você não precisa fazer muito trabalho de configuração para se beneficiar deles.

Avise-me se isso ajudar.

Todos 9 comentários

@ajorians Bom dia! Pelo que entendi seu exemplo, parece que já temos o que você precisa. O método de fixação Build() é, na verdade, criar uma cópia imutável do gráfico interno, para que você possa aplicar personalizações "uma vez" no topo da AutoFixação sem persisti-las.

Reveja esta demonstração do parque infantil:

`` `c #
modelo de classe pública
{
public int Id {get; definir; }
public string Name {get; definir; }
}

[Facto]
public void Demo ()
{
var fixture = new Fixture ();
fixture.Customize(c => c. Com (m => m.Id, 42));

var value1 = fixture.Create<Model>();
Assert.Equal(42, value1.Id);
Assert.NotNull(value1.Name);

var value2 = fixture.Build<Model>()
    .With(m => m.Id, (int genInt) => genInt * 2 /* make it always even */)
    .Without(x => x.Name)
    .Create();
Assert.NotEqual(42, value2.Id);
Assert.Equal(value2.Id % 2, 0);
Assert.Null(value2.Name);

var value3 = fixture.Create<Model>();
Assert.Equal(42, value3.Id);
Assert.NotNull(value3.Name);

}
`` `

Deixe-nos saber se ele atende às suas necessidades. Caso contrário, descreva mais especificamente por que isso não funciona para você, para que possamos tentar ajudar.

Obrigado!

Olá @zvirja ,

Dia bom!

Se me é permitido; nossa classe Model não teria um set e só seria configurável através do construtor. Então, se você substituí-lo por este:

public class Model
{
   public Model( int id, string name )
   {
      Id = id;
      Name = name;
   }

   public int Id { get; }
   public string Name { get; }
}

Desculpe por não especificar isso originalmente.

Portanto, pelo que entendo as coisas, tenho que adicionar personalizações para fazer os parâmetros do construtor. E essas personalizações estão antes de Build() .

Se estiver interessado em algumas das razões pelas quais fazemos muito no construtor e sem setters, podemos usar a palavra-chave readonly e ter certeza de que as variáveis ​​de membro estão definidas e não podem ser desmarcadas no momento em que um método é chamado.

Espero que isso faça mais sentido. Obrigado por ajudar com isso! :risonho:

@ajorians Eu entendi seu problema corretamente, que você tem um modelo com uma grande quantidade de argumentos de entrada, então você gostaria de personalizar apenas um deles, sem ter que se preocupar explicitamente com isso. Direito?

Caso contrário, você sempre pode escrever algo como
c# var value2 = fixture.Build<Model>() .FromFactory((int id, string name) => new Model(id, name)) .Create();

Infelizmente, ele não se adapta muito bem com o número crescente de parâmetros 😟

@ajorians Eu entendi seu problema corretamente, que você tem um modelo com uma grande quantidade de argumentos de entrada, então você gostaria de personalizar apenas um deles, sem ter que se preocupar explicitamente com isso. Direito?

Sim, isso é exatamente correto.

Infelizmente, ele não se adapta muito bem com o número crescente de parâmetros 😟

Felizmente, Freeze funciona para muitas de nossas classes com um número crescente de parâmetros. Mas sim, a classe que eu tinha em mente, onde estou usando as personalizações para fazer parâmetros do construtor, atualmente tem mais de 30 parâmetros:
image

E provavelmente crescerá um adicional de 4 ou mais antes do final deste ano.

Eu entendo que isso pode ser incomum.

Mas, sim, tenho uma grande quantidade de argumentos de construtor de entrada crescentes e gostaria de personalizar apenas um (ou alguns) deles sem pensar no resto. Sem ter esse tipo registrado ou congelado para todo o teste. E de uma forma concisa, pois adicionar uma personalização envolve cerca de 5 linhas de código.

Felizmente, sou capaz de fazer tudo isso; mas a forma como fiz isso envolveu a criação de um método de extensão clone no meu código do lado do cliente.

Espero que tudo isso faça sentido. Obrigado por olhar isso comigo: smiley:

@ajorians Eu estava prestes a encerrar o problema devido à inatividade, mas não consegui passar por aquele construtor gigantesco.
Tenho que perguntar, você considerou mudar seu design?

Olhando para o seu construtor, posso ver vários problemas e soluções potenciais para o seu problema.

Em primeiro lugar, você está misturando os conceitos de entidades novas e injetáveis.
A solução mais direta é mover suas dependências injetáveis ​​(também conhecidas como interfaces de serviço) para propriedades e injetá-las usando injeção de propriedade.
Isso resolverá seu problema imediato de ter que copiar Fixture e tornará o construtor mais gerenciável.

O segundo é lidar com o cheiro de código de injeção excessiva .
Isso significa mover os serviços mais comumente usados ​​juntos para abstrações separadas.
Isso deve ajudar seu projeto a respeitar o SRP e, por extensão, deve reduzir a enorme quantidade de configuração que você precisa fazer no momento, em seus testes.

Como a classe se chama ViewModel , presumo que você tenha um aplicativo MVVM. É provável que você possa desacoplar ainda mais seu aplicativo, introduzindo um modelo de sistema de mensagens. Talvez um agregador de eventos. A maioria dos frameworks MVVM os tem embutidos, então você não precisa fazer muito trabalho de configuração para se beneficiar deles.

Avise-me se isso ajudar.

Olá @aivascu ,

@ajorians , estava prestes a encerrar o problema devido à inatividade

Se houver algo que eu possa fazer, é só me avisar. Por ser uma solicitação de recurso que posso ver até ser implementada, ela pode ter uma quantidade razoável de inatividade.

Tenho que perguntar, você considerou mudar seu design?

No momento, estamos considerando nos tornar uma aplicação de prisma . Isso ajudaria com alguns dos itens que você mencionou. Mas, infelizmente, não nos vejo mudando a filosofia de apenas colocar tudo no construtor de forma que possa ser verificado null uma vez e ser readonly e sempre ser diferente de null ao longo da vida da classe. Precisamos fazer algo. Mas não acho que isso vá melhorar no próximo ano ou assim.

Avise-me se isso ajudar.

Tudo isso ajuda. Vou informar a minha equipe que até mesmo as pessoas no mundo acham que precisamos considerar a possibilidade de fazer mudanças.

Mas, se eu puder perguntar, é razoável que IFixture tenha um Clone ou método semelhante? Ou, de alguma forma, permitir que as pessoas adicionem uma personalização e, em seguida, pop a última personalização adicionada ou, de alguma forma, retornem ao estado anterior à adição de uma personalização? Eu concordo que entender o cenário que leva a esse comportamento desejado é importante e certamente o cenário tem muitos problemas em primeiro lugar. E vejo que você não deseja adicionar complexidade adicional, a menos que haja um bom caso de uso. Bem, você pode escolher o que acontece a seguir, mas se eu puder ajudar, é só me avisar.

Obrigado por ler e considerar: smiley:

@ajorians minha opinião é que não devemos adicionar um .Clone() método para o Fixture classe. A meu ver, deve haver apenas um dispositivo de teste a qualquer momento para qualquer teste individual.

Houve alguns pedidos para implementar um recurso de descongelar / ejetar / congelar uma vez no passado, mas ninguém conseguiu implementá-lo.
Acho que o motivo é que não há uma maneira trivial de identificar com segurança qual construtor vai retornar o valor congelado ou qual foi o último valor injetado.

O que você pode fazer é implementar um relé que ignore as solicitações após uma certa contagem de resoluções bem-sucedidas, como abaixo. Você também pode atualizar a implementação para pular uma certa quantidade de solicitações. O resto deve ser coberto pela personalização que você usou na postagem original.

public class CountingRelay : ISpecimenBuilder
{
    public CountingRelay(ISpecimenBuilder builder, int maxCount)
    {
        this.MaxCount = maxCount;
        this.Builder = builder;
    }

    public int Count { get; private set; }
    public int MaxCount { get; }
    public ISpecimenBuilder Builder { get; }

    public object Create(object request, ISpecimenContext context)
    {
        if (this.Count == this.MaxCount) return new NoSpecimen();
        var result = this.Builder.Create(request, context);
        if (!(result is NoSpecimen)) this.Count++;
        return result;
    }
}

Olá @aivascu ,

@ajorians minha opinião é que não devemos adicionar um .Clone() método para o Fixture classe.

OK. Bem, obrigado por considerar.

O que você pode fazer é implementar um relé que ignore as solicitações após uma certa contagem de resoluções bem-sucedidas

Vou dar uma olhada.

Obrigado novamente! :risonho:

@ajorians Eu criei uma solicitação de recurso mais formal # 1214 para redefinição de personalização explícita e automática. Você pode acompanhar o progresso desse recurso lá.

Esta página foi útil?
0 / 5 - 0 avaliações

Questões relacionadas

ploeh picture ploeh  ·  7Comentários

Ephasme picture Ephasme  ·  3Comentários

JoshKeegan picture JoshKeegan  ·  6Comentários

zvirja picture zvirja  ·  8Comentários

Ridermansb picture Ridermansb  ·  4Comentários