Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I generate objects when there are multiple paths to a child entity?

Tags:

c#

autofixture

I'm working on an app with a domain model similar to this, where a LineItem can be referenced both from an Order as well as from a Shipment.

Diagram

If I'm using AutoFixture to generate an Order, how can I use the same set of LineItems for both order.LineItems and order.Shipments*.ItemShipments*.LineItem?

Theoretically, the following test ought to pass:

var fullyShippedOrder = fixture.CreateAnonymous<Order>();
var shippedLineItems = fullyShippedOrder.Shipments
    .SelectMany(o => o.ItemShipment, (s, i) => i.LineItem)
    .Distinct();
Assert.EqualCollection(fullyShippedOrder.LineItems, shippedLineItems);

... although I would also want to be able to generate partially-shipped orders depending on the test.

(There's a solid argument to be made that a line item on an Order and a line item on a Shipment are different things and I shouldn't be using the same class to represent them. However, the data I'm working with comes from a legacy system and there isn't much that can be done about it. )

like image 749
Brant Bobby Avatar asked Dec 18 '12 19:12

Brant Bobby


1 Answers

As you said, the friction you're experiencing in your tests is most likely a sign of a design problem in the domain model. Ideally, you should listen to your tests and fix the problem at its root. However, granted that it isn't feasible in this case, here's a workaround.

You can configure the Fixture to always return LineItem objects out of a fixed sequence by using the Register<T>(Func<T> creator) method.

Here's an example packaged up in a customization:

public class GenerateLineItemFromFixedSequence : ICustomization
{
    public void Customize(IFixture fixture)
    {
        var items = CreateFixedLineItemSequence(fixture);
        fixture.Register(() => GetRandomElementInSequence(items));
    }

    private static IEnumerable<LineItem> CreateFixedLineItemSequence(IFixture fixture)
    {
        return fixture.CreateAnonymous<LineItem[]>();
    }

    private static LineItem GetRandomElementInSequence(IEnumerable<LineItem> items)
    {
        var randomIndex = new Random().Next(0, items.Count());
        return items.ElementAt(randomIndex);
    }
}

In order to apply this behavior in the context of a test, just add the customization to the Fixture object:

fixture.Customize(new GenerateLineItemFromFixedSequence());

Similarly, you could create other customizations that generate the fixed sequence of LineItem objects in different states, like the partially-shipped orders you mentioned, and use them in different tests.

An observation

It's interesting to note that this customization can be made generic, since the algorithm itself isn't coupled with the type of objects being created. This would effectively transform it into a strategy.

So, modifying the customization by introducing a generic parameter:

public class GenerateFromFixedSequence<T> : ICustomization
{
    public void Customize(IFixture fixture)
    {
        var items = CreateFixedSequence(fixture);
        fixture.Register(() => GetRandomElementInSequence(items));
    }

    private static IEnumerable<T> CreateFixedSequence(IFixture fixture)
    {
        return fixture.CreateAnonymous<T[]>();
    }

    private static T GetRandomElementInSequence(IEnumerable<T> items)
    {
        var randomIndex = new Random().Next(0, items.Count());
        return items.ElementAt(randomIndex);
    }
}

would allow you to use it for different objects:

fixture.Customize(new GenerateFromFixedSequence<LineItem>());
fixture.Customize(new GenerateFromFixedSequence<Order>());
fixture.Customize(new GenerateFromFixedSequence<Shipment>());
like image 78
Enrico Campidoglio Avatar answered Nov 14 '22 20:11

Enrico Campidoglio