Unable to create a customized object using Auto Fixture in c# xunit

121 Views Asked by At

I have below class and a dictionary of category item mappings

public class InfoClass
    {
        public InfoClass()
        {
            this.InfoID = string.Empty;
            this.EDDetails = string.Empty;
            this.EVDetails = string.Empty;
        }
        public int ID { get; set;}
        public string InfoID { get; set;}
        public string EDDetails { get; set; }
        public string EVDetails { get; set; }
    }


    private static Dictionary<string, List<string>> ItemsMap = new Dictionary<string, List<string>>
            {
                { Constants.ITEMCATEGORY_ED, new List<string> { "XYZ", "IDASAS" } },
                { Constants.ITEMCATEGORY_ERANDRE, new List<string> { "SAS", "PQR" } }
            
            };

I want to create the InfoClass mock object in such a way that, the InfoID of InfoClass should be random from the list inside the Dictionary. Based on that category of the selected InfoID, assign a value for EDDetails or EVDetials.

For example, If the random value of ReportId is "XYZ", then category of InfoID is ITEMCATEGORY_ED, then assign {Generate data based on a logic} value to EDDetails property and all the other properties EVDetails should be empty.

To achieve this, i tried creating a Customization using below.

public class InfoClassCustomization : ICustomization
    {
        private readonly Dictionary<string, List<string>> ItemsMap;

        public ArchivedOrderLineItemInfoCustomization(Dictionary<string, List<string>> ItemsMap)
        {
            this.ItemsMap = ItemsMap;
        }

        public virtual void Customize(IFixture fixture)
        {
            fixture.Customize<InfoClass>(c => c
                .Do(o =>
                {
                    // Flatten your dictionary to a list of tuples
                    var keyValueList = ItemsMap.SelectMany(kvp => kvp.Value.Select(v => (Key: kvp.Key, Value: v))).ToList();

                    // Pick a random tuple
                    var InfoIdPair = keyValueList[new Random().Next(keyValueList.Count)];

                    // Set the ReportID from the randomly selected tuple
                    o.ReportID = InfoIdPair.Value;


                    // Set the correct property based on the key of the randomly selected tuple
                    switch (InfoIdPair.Key)
                    {
                        case Constants.ITEMCATEGORY_ED:
                            o.EDDetails = JsonConvert.SerializeObject(fixture.Create<EdDetails>());
                            break;
                        case Constants.ITEMCATEGORY_ERANDRE:
                            o.EVDetails = JsonConvert.SerializeObject(fixture.Create<EvDetails>());
                            break;
                    }
                }));
        }
    }



var fixture = new Fixture().Customize(new InfoClassCustomization(ItemsMap));

var obj = fixture.Create<InfoClass>();

But this didnt worked. Didn't even got any exception. So tried below

public static InfoClass GetItemInfo()
        {
            var fixture = new Fixture();
            InfoClass o = new InfoClass();
            // Flatten your dictionary to a list of tuples
                    var keyValueList = ItemsMap.SelectMany(kvp => kvp.Value.Select(v => (Key: kvp.Key, Value: v))).ToList();

                    // Pick a random tuple
                    var InfoIdPair = keyValueList[new Random().Next(keyValueList.Count)];

                    // Set the ReportID from the randomly selected tuple
                    o.ReportID = InfoIdPair.Value;


                    // Set the correct property based on the key of the randomly selected tuple
                    switch (InfoIdPair.Key)
                    {
                        case Constants.ITEMCATEGORY_ED:
                            o.EDDetails = JsonConvert.SerializeObject(fixture.Create<EdDetails>());
                            break;
                        case Constants.ITEMCATEGORY_ERANDRE:
                            o.EVDetails = JsonConvert.SerializeObject(fixture.Create<EvDetails>());
                            break;
                    }

            return o;
        }

var fixture = new Fixture();
            fixture.Customize<ArchivedOrderLineItemInfo>(c => c
            .Do(o => o= GetItemInfo()));

var mockedvalue = fixture.Create<InfoClass>();

With the second approach, while i debug, i can see the data/values assigned to the properties inside the GetItemInfo method. But once it returned the object and assigned to "mockedvalue", the data generated inside the method is gone and a fresh mock data with out the custom logic is generated.

Is there any issue with my logic ? or am i missing something ?

2

There are 2 best solutions below

3
Michał Turczyn On BEST ANSWER

Thinking of it other way, more from unit test perspective, I have modified your solution with ICUstomization. Here is how I implemented it:

public class InfoClassCustomization : ICustomization
{
    private readonly Dictionary<string, List<string>> ItemsMap;

    public InfoClassCustomization(Dictionary<string, List<string>> ItemsMap)
    {
        this.ItemsMap = ItemsMap;
    }

    public void Customize(IFixture fixture)
    {
        // Flatten your dictionary to a list of tuples
        var keyValueList = ItemsMap.SelectMany(kvp => kvp.Value.Select(v => (Key: kvp.Key, Value: v))).ToList();

        // Pick a random tuple
        var InfoIdPair = keyValueList[new Random().Next(keyValueList.Count)];

        // Set the ReportID from the randomly selected tuple
        var reportID = InfoIdPair.Value;
        var edDetails = JsonConvert.SerializeObject(fixture.Create<EdDetails>());
        var evDetails = JsonConvert.SerializeObject(fixture.Create<EvDetails>());

        fixture.Customize<InfoClass>(c =>
            {
                var composer = c.With(x => x.ReportID, reportID);
                // Set the correct property based on the key of the randomly selected tuple
                switch (InfoIdPair.Key)
                {
                    case Constants.ITEMCATEGORY_ED:
                        return composer.With(x => x.EDDetails, edDetails);
                    case Constants.ITEMCATEGORY_ERANDRE:
                        return composer.With(x => x.EVDetails, evDetails);
                    default:
                        return c;
                }
            });
    }
}

And here is sample unit test (I think you have missed call to Fixture's Customize(new InfoClassCustomization(dict));):

public class UnitTest1
{
    [Fact]
    public void Test1()
    {
        // Arrange
        var dict = new Dictionary<string, List<string>>
        {
            { Constants.ITEMCATEGORY_ED, new List<string> { "XYZ", "IDASAS" } },
            { Constants.ITEMCATEGORY_ERANDRE, new List<string> { "SAS", "PQR" } }
        };

        var autoFixture = new Fixture()
            .Customize(new InfoClassCustomization(dict));

        // Act
        var infoClass = autoFixture.Create<InfoClass>();

        // Assert
        var possibleReportIds = dict.Values.SelectMany(x => x);

        Assert.NotNull(
            possibleReportIds
                .FirstOrDefault(x => x == infoClass.ReportID));
    }
}

EDIT Accordingly to comments, above method will create all objects with the same ReportId. To correct that we can use Fixture.Register method, as below:

public class InfoClassCustomization : ICustomization
{
    private readonly Dictionary<string, List<string>> ItemsMap;

    public InfoClassCustomization(Dictionary<string, List<string>> ItemsMap)
    {
        this.ItemsMap = ItemsMap;
    }

    public void Customize(IFixture fixture)
    {
        // Flatten your dictionary to a list of tuples
        var keyValueList = ItemsMap.SelectMany(kvp => kvp.Value.Select(v => (Key: kvp.Key, Value: v))).ToList();

        fixture.Register(() =>
        {
            // Pick a random tuple
            var InfoIdPair = keyValueList[new Random().Next(keyValueList.Count)];

            // Set the ReportID from the randomly selected tuple
            var reportID = InfoIdPair.Value;
            var edDetails = "EDDetails";// JsonConvert.SerializeObject(fixture.Create<EdDetails>());
            var evDetails = "EVDetails";// JsonConvert.SerializeObject(fixture.Create<EvDetails>());

            return new InfoClass
            {
                ReportID = reportID,
                EDDetails = edDetails,
                EVDetails = evDetails,
                ID = fixture.Create<int>(),
                InfoID = fixture.Create<string>(),
            };
        });
    }
}

Now below sample unit test passes:

public class UnitTest1
{
    [Fact]
    public void Test1()
    {
        var dict = new Dictionary<string, List<string>>
        {
            { Constants.ITEMCATEGORY_ED, new List<string> { "XYZ", "IDASAS" } },
            { Constants.ITEMCATEGORY_ERANDRE, new List<string> { "SAS", "PQR" } }
        };

        var autoFixture = new Fixture()
            .Customize(new InfoClassCustomization(dict));

        var infoClasses = autoFixture.CreateMany<InfoClass>(10);

        var possibleReportIds = dict.Values.SelectMany(x => x).ToArray();

        foreach (var ic in infoClasses)
        {
            Assert.True(possibleReportIds.Contains(ic.ReportID));
        }

        Assert.True(infoClasses.Select(ic => ic.ReportID).Distinct().Count() > 1);
    }
}
4
Michał Turczyn On

For me it looks like you have additional creation logic of your InfoClass, so i would separate that logic in some sort of InfoClassHelper class, as below:

public class InfoClassHelper
{
    private static Dictionary<string, List<string>> ItemsMap = new Dictionary<string, List<string>>
    {
        { Constants.ITEMCATEGORY_ED, new List<string> { "XYZ", "IDASAS" } },
        { Constants.ITEMCATEGORY_ERANDRE, new List<string> { "SAS", "PQR" } }
    };

    public static string[] AvailableReports { get; } = ItemsMap.Values.SelectMany(x => x).ToArray();

    public static string GetInfoIdBasedOnReport(string infoId)
    {
        if (!AvailableReports.Contains(infoId)) return null;

        return ItemsMap
            .First(x => x.Value.Contains(infoId, StringComparer.OrdinalIgnoreCase))
            .Key;
    }
}

then using this class it would be much easier to create objects of InfoClass using AutoFixture, as below:

var autoFixture = new Fixture();

var index = new Random().Next(InfoClassHelper.AvailableReports.Length);
var randomReportId = InfoClassHelper.AvailableReports[index];
var infoClass = autoFixture
    .Build<InfoClass>()
    .With(x => x.InfoID, InfoClassHelper.GetInfoIdBasedOnReport(randomReportId))
    .Create();

EDIT

After reading comment, you could implement factory class like below:

public class InfoClassFactory
{
    private static Dictionary<string, List<string>> ItemsMap = new Dictionary<string, List<string>>
    {
        { Constants.ITEMCATEGORY_ED, new List<string> { "XYZ", "IDASAS" } },
        { Constants.ITEMCATEGORY_ERANDRE, new List<string> { "SAS", "PQR" } }
    };

    public static string[] AvailableReports { get; } = ItemsMap.Values.SelectMany(x => x).ToArray();

    private static string CreateInfoClass(string infoId)
    {
        return new InfoClass
        {
            EVDetails = GetEVDetailsBasedOnReport(infoId),
            EDDetails = GetEDDetailsBasedOnReport(infoId),
            EVDetails = GetInfoIdBasedOnReport(infoId),
        }
    }

    private static string GetEDDetailsBasedOnReport(string infoId)
    {
        // logic here
    }

    private static string GetEVDetailsBasedOnReport(string infoId)
    {
        // logic here
    }

    private static string GetInfoIdBasedOnReport(string infoId)
    {
        if (!AvailableReports.Contains(infoId)) return null;

        return ItemsMap
            .First(x => x.Value.Contains(infoId, StringComparer.OrdinalIgnoreCase))
            .Key;
    }
}