2

I have following classes:

public class Foo
{
    public List<DescriptionInfo> Descriptions { get; set; }
}


public class DescriptionInfo
{
    public int LanguageId { get; set; }
    public string Value { get; set; }
}

I want to create Foo instance, using Autofixture. However, the LanguageId must come from a predefined list. Therefore I've created following customization:

public class LanguageIdSpecimenBuilder : ISpecimenBuilder
{
    private static readonly List<int> LanguageIds = new List<int> {
        1,
        2,
        666,
    };

    public object Create(object request, ISpecimenContext context)
    {
        var info = request as PropertyInfo;
        if (info != null)
        {
            if (info.Name == "LanguageID")
            {
                return LanguageIds.GetRandomElement();
            }
        }
        return new NoSpecimen(request);
    }
}

Now everything is good:

Fixture fixture = new Fixture();
fixture.Customizations.Add(new LanguageIdSpecimenBuilder());
var foo = fixture.Create<Foo>(); 

However, there is one more requirement: there must be no duplicated entries for one language ID. How can I achieve this?

EDIT: For example, valid instances are:

Foo1:
 - LanguageId: 1, Value: "english description_ae154c"
 - LanguageId: 2, Value: "zuzulu_510b7g"

Foo2:
 - LanguageId: 1, Value: "english description_87f5de"
 - LanguageId: 666, Value: "chinese_35e450"
 - LanguageId: 2, Value: "zuzulu_fe830d"

Invalid instance:

Foo1:
 - LanguageId: 1, Value: "_04dcd6"
 - LanguageId: 1, Value: "_66ccc4"
 - LanguageId: 2, Value: "zuzulu_c05b0f"
piotrwest
  • 2,098
  • 23
  • 35

2 Answers2

3

First of all - let me suggest your POCes can be more precise. If you don't allow descriptions with same LanguageID in the list, you can remodel your POCes like:

public class Foo
{
    public ISet<DescriptionInfo> Descriptions { get; set; }
}

public class DescriptionInfo
{
    public int LanguageID { get; set; }
    public string Value { get; set; }

    public override bool Equals(object obj)
    {
        var anotherInfo = (DescriptionInfo)obj;
        return anotherInfo.LanguageID == LanguageID;
    }

    public override int GetHashCode()
    {
        return LanguageID;
    }
}

And the problem is gone:

fixture.Customizations.Add(new TypeRelay(typeof(ISet<DescriptionInfo>),
  typeof(HashSet<DescriptionInfo>)));
fixture.Customizations.Add(new LanguageIdSpecimenBuilder());
var foo = fixture.Create<Foo>();

If you can't/ don't want to remodel your POCes, you should focus in the custom builder on the property you want to set right - Descriptions.

public object Create(object request, ISpecimenContext context)
{
    var info = request as PropertyInfo;
    if (info != null && info.Name == "Descriptions" && info.DeclaringType == typeof(Foo))
    {
        if (info.Name == "Descriptions")
        {
            return context.Create<List<DescriptionInfo>>()
             .GroupBy(g => g.LanguageId)
             .Select(g => g.First())
             .ToList();
         }
    }
    if (info != null && info.Name == "LanguageId" && info.DeclaringType == typeof(DescriptionInfo))
    {
        return LanguageIds.GetRandomElement();
    }
    return new NoSpecimen(request);
}

Final note - you're code snippet is wrong - string comparison is case sensitive; so it should be info.Name == "LanguageId", also it is great idea to check the type that declares the property - not just the property name.

Ondrej Svejdar
  • 21,349
  • 5
  • 54
  • 89
  • Thanks for detailed answer. Indeed there is a case error & checking declaring type is a very good idea. – piotrwest Feb 22 '16 at 15:32
2

Since you want the ID values to be unique on a single Foo instance, you'll probably be best off customizing the Foo type itself.

You'll probably want to let AutoFixture deal with the other parts of Foo, except the DescriptionInfo values. One way to do this is to create an ISpecimenCommand, which can be used as a post-processor of generated values:

public class UniqueIDsOnFooCommand : ISpecimenCommand
{
    private static readonly int[] languageIds = new [] { 1, 2, 666, };
    private static readonly Random random = new Random();

    public void Execute(object specimen, ISpecimenContext context)
    {
        var foo = specimen as Foo;
        if (foo == null)
            return;

        foreach (var t in
            foo.Descriptions.Zip(Shuffle(languageIds), Tuple.Create))
        {
            var description = t.Item1;
            var id = t.Item2;
            description.LanguageId = id;
        }            
    }

    private static IEnumerable<T> Shuffle<T>(IReadOnlyCollection<T> source)
    {
        return source.OrderBy(_ => random.Next());
    }
}

This implementation uses a ready-made array of well-known language IDs, shuffles them, and then zips them with the descriptions.

You can package this command in a Customization that changes the behaviour of the Foo type:

public class FooCustomization : ICustomization
{
    public void Customize(IFixture fixture)
    {
        fixture.Customizations.Add(
            SpecimenBuilderNodeFactory.CreateTypedNode(
                typeof(Foo),
                new Postprocessor(
                    new MethodInvoker(
                        new ModestConstructorQuery()),
                    new CompositeSpecimenCommand(
                        new AutoPropertiesCommand(),
                        new UniqueIDsOnFooCommand()))));
    }
}

The following test passes:

[Fact]
public void CreateFoos()
{
    var fixture = new Fixture().Customize(new FooCustomization());
    fixture.Behaviors.Add(new TracingBehavior());

    var foos = fixture.CreateMany<Foo>();

    Assert.True(
        foos.All(f => f
            .Descriptions
            .Select(x => x.LanguageId)
            .Distinct()
            .Count() == f.Descriptions.Count),
        "Languaged IDs should be unique for each foo.");
}

The created Foo values look like this:

  Foo:
  - LanguageId: 1, Value: "Valueefd1268c-56e9-491a-a43d-3f385ea0dfd8"
  - LanguageId: 666, Value: "Value461d7130-7bc0-4e71-96a8-432c019567c9"
  - LanguageId: 2, Value: "Valuee2604a80-f113-485c-8276-f19a97bca505"

  Foo:
  - LanguageId: 2, Value: "Value66eee598-1895-4c0d-b3c6-4262711fe394"
  - LanguageId: 666, Value: "Valuef9a58482-13c6-4491-a690-27b243af6404"
  - LanguageId: 1, Value: "Valueeb133071-dad7-4bff-b8ef-61d3505f1641"

  Foo:
  - LanguageId: 1, Value: "Valuea1161dc6-75e5-45a3-9333-f6bca01e882b"
  - LanguageId: 666, Value: "Value31332272-5b61-4d51-8572-172b781e5f6b"
  - LanguageId: 2, Value: "Value83f80569-277d-49b2-86e5-be256075835e"

All that said, consider a different design that doesn't involve settable collection properties.

Community
  • 1
  • 1
Mark Seemann
  • 225,310
  • 48
  • 427
  • 736
  • Also a nice solution, even it seems to me that this is a little less "hacky" than @ondrej-svejdar answer. However because he was first, I'll keep his answer as accepted. – piotrwest Feb 22 '16 at 18:43
  • 1
    @piotrwest No worries. The important part is that you got a useful answer to your question. – Mark Seemann Feb 22 '16 at 19:25