Search code examples
c#unit-testingautofixture

Collection size from attribute for Autofixture declarative autodata parameter


How to a specify a list/enumerable's length/size using an attribute on a property passed into a test using Autofixture's declarative parameter style?

I want to be able to make this test pass without moving parameters into the test body.

        [Theory, AutoData]
        public void CollectionSizeTest(
            List<int> defaultSize,
            List<int> customSize,
            List<int> customSize2,
            IEnumerable<string> empty
        )
        {
            Assert.Equal(3, defaultSize.Count);
            Assert.Equal(5, customSize.Count);
            Assert.Equal(6, customSize2.Count);
            Assert.Empty(empty);
        }

Solution

  • You can create a custom attribute for this, such as this CollectionSizeAttribute:

            [Theory, AutoData]
            public void CollectionSizeTest(
                List<int> defaultSize,
                [CollectionSize(5)] List<int> customSize,
                [CollectionSize(6)] List<int> customSize2,
                [CollectionSize(0)] IEnumerable<string> empty,
                List<string> defaultSize2
            )
            {
                Assert.Equal(3, defaultSize.Count);
                Assert.Equal(5, customSize.Count);
                Assert.Equal(6, customSize2.Count);
                Assert.Empty(empty);
                Assert.Equal(3, defaultSize2.Count);
            }
    
            public class CollectionSizeAttribute : CustomizeAttribute
            {
                private readonly int _size;
    
                public CollectionSizeAttribute(int size)
                {
                    _size = size;
                }
    
                public override ICustomization GetCustomization(ParameterInfo parameter)
                {
                    if (parameter == null) throw new ArgumentNullException(nameof(parameter));
    
                    var objectType = parameter.ParameterType.GetGenericArguments()[0];
    
                    var isTypeCompatible =
                        parameter.ParameterType.IsGenericType
                        && parameter.ParameterType.GetGenericTypeDefinition().MakeGenericType(objectType).IsAssignableFrom(typeof(List<>).MakeGenericType(objectType))
                    ;
                    if (!isTypeCompatible)
                    {
                        throw new InvalidOperationException($"{nameof(CollectionSizeAttribute)} specified for type incompatible with List: {parameter.ParameterType} {parameter.Name}");
                    }
    
                    var customizationType = typeof(CollectionSizeCustomization<>).MakeGenericType(objectType);
                    return (ICustomization) Activator.CreateInstance(customizationType, parameter, _size);
                }
    
                public class CollectionSizeCustomization<T> : ICustomization
                {
                    private readonly ParameterInfo _parameter;
                    private readonly int _repeatCount;
    
                    public CollectionSizeCustomization(ParameterInfo parameter, int repeatCount)
                    {
                        _parameter = parameter;
                        _repeatCount = repeatCount;
                    }
    
                    public void Customize(IFixture fixture)
                    {
                        fixture.Customizations.Add(new FilteringSpecimenBuilder(
                            new FixedBuilder(fixture.CreateMany<T>(_repeatCount).ToList()),
                            new EqualRequestSpecification(_parameter)
                        ));
                    }
                }
            }
    

    This causes the parameter to be created as a list with the given size by calling fixture.CreateMany<T>(_repeatCount).