Ignore virtual properties
Asked Answered
Z

4

15

We have MVC4 project with Entity Framework for storage. For our tests we recently started using Autofixture and it is really awesome.

Our models graph is very deep and usually creating one object by AutoFixture creates the whole graph: Person -> Team -> Departments -> Company -> Contracts -> .... etc.

The problem with this is time. Object creation takes up to one second. And this leads to slow tests.

What I find myself doing a lot is things like this:

        var contract = fixture.Build<PersonContract>()
            .Without(c => c.Person)
            .Without(c => c.PersonContractTemplate)
            .Without(c => c.Occupation)
            .Without(c => c.EmploymentCompany)
            .Create<PersonContract>();

And this works and it is quick. But this over-specification makes tests hard to read and sometimes I loose the important details like .With(c => c.PersonId, 42) in the list of unimportant .Without().

All these ignored objects are navigational properties for Entity Framework and all are virtual.

Is there a global way to tell AutoFixture to ignore virtual members?

I have tried creating ISpecimentBuilder, but no luck:

public class IgnoreVirtualMembers : ISpecimenBuilder
{
    public object Create(object request, ISpecimenContext context)
    {

        if (request.GetType().IsVirtual // ?? this does not exist )
        {
             return null;
        }
    }
}

I can't seem to find a way in ISpecimenBuilder to detect that object we are constructing is a virtual member in another class. Probably ISpecimenBuilder this is not the right place to do this. Any other ideas?

Zuckerman answered 29/3, 2013 at 15:1 Comment(0)
Z
28

Read a bit more on Mark's blog (this particularly) I found the way to do what I want:

/// <summary>
/// Customisation to ignore the virtual members in the class - helps ignoring the navigational 
/// properties and makes it quicker to generate objects when you don't care about these
/// </summary>
public class IgnoreVirtualMembers : ISpecimenBuilder
{
    public object Create(object request, ISpecimenContext context)
    {
        if (context == null)
        {
            throw new ArgumentNullException("context");
        }

        var pi = request as PropertyInfo;
        if (pi == null)
        {
            return new NoSpecimen(request);
        }

        if (pi.GetGetMethod().IsVirtual)
        {
            return null;
        }
        return new NoSpecimen(request);
    }
}

And you can wrap these into customisation:

public class IgnoreVirtualMembersCustomisation : ICustomization
{
    public void Customize(IFixture fixture)
    {
        fixture.Customizations.Add(new IgnoreVirtualMembers());
    }
}

So in your test you just do:

var fixture = new Fixture().Customize(new IgnoreVirtualMembersCustomisation());

and go create your complex models.

Enjoy!

Zuckerman answered 29/3, 2013 at 16:34 Comment(4)
Saved the day. However, at least in the US, Customisations is spelled Customizations. :)Coset
I know, pesky z gets into all *sations. In UK it is spelled properly, with "s" -)))Zuckerman
This is a fantastic solution. I was able to drop the classes into my project and used them with Moq right away. Excellent job!Divagate
Worked for me 9 years later. However the NoSpecimen class constructor that takes a parameter is obsolete. I removed the argument on the 'return new NoSpecimen(request)' line and seems to work fine.Mnemosyne
S
3

I had this same problem and decided to go one step further and create a customization to lazy-load navigation properties. The project is on Github and NuGet.

Consider the simple object graph below which has a circular dependency:

class Foo
{
    public int Id { get; set; }
    public int BarId { get; set; }
    public virtual Bar Bar { get; set; }
}

class Bar
{
    public int Id { get; set; }
    public int FooId { get; set; }
    public virtual Foo Foo { get; set; }
}

With this customization, calling var foo = fixture.Create<Foo>() will create an object of type Foo. Calling the foo.Bar getter will use DynamicProxy and AutoFixture to create an instance of Bar on the fly and assign it to that property. Subsequent calls to foo.Bar return the same object.

N.B. the customization is not smart enough to set foo.Bar.Foo = foo - this must be done manually if needed

Synopsize answered 24/5, 2014 at 8:50 Comment(4)
@AlexFoxGill, your project no longer is compatible with the latest AutoFixture 3.3+. When attempting to run UT with your customization an error is generated that the assembly is not correct.Wo
@AlexFoxGill any plans to address? Hopefully it is just a need to update references to Ploeh.AutoFixture then rebuild and not much else.Wo
@Wo yes - I've added an issue on the GitHub tracker and will get to it when I can (or someone else can have a crack)Synopsize
@Wo I have updated references to AutoFixture.3.37.0 in the latest NuGet build (0.5.0)Synopsize
D
1

Following the trailmax's answer, I want to note that this solution is actually setting the property to null. And while it may be okay for most use cases, it's actually overwriting the original value that may be initialized like this

public virtual ICollection<Entity> Entities { get; set; } = new List<Entity>();

or be set in factory method before being provided to a fixture:

fixture.Build<Entity>()
    .FromFactory(Entity.Create) // controllably adds certain objects in navigation for example
    .Create();

For these cases there is an OmitSpecimen signal type:

If NoSpecimen is like a continue in a loop - skips the rest and starts next iteration (next specimen builder), OmitSpecimen is like break in a loop - stops all further processing for this property specimen

So, the customization that ignores property like .Without() does would be looking like this:

/// <summary>
/// Customisation to ignore the virtual members in the class - helps ignoring the navigational 
/// properties and makes it quicker to generate objects when you don't care about these
/// </summary>
public class IgnoreVirtualMembers : ISpecimenBuilder
{
    public object Create(object request, ISpecimenContext context)
    {
        ArgumentNullException.ThrowIfNull(nameof(request));

        if (request is not PropertyInfo pi) return new NoSpecimen(); // aka 'continue' in a loop

        var getter = pi.GetGetMethod();

        if (getter == null) return new NoSpecimen();

        if (!getter.IsVirtual) return new NoSpecimen();

        return new OmitSpecimen(); // aka 'break' in a loop
    }
}
Denmark answered 8/8 at 15:28 Comment(0)
C
0

I had a weird problem with this code because all my entities inherit from a base model like this:

public interface IBaseModel
{
    Guid Id { get; set; }
    DateTime? DateCreated { get; set; }
    Guid? CreatedBy { get; set; }
    DateTime? DateLastModified { get; set; }
    Guid? LastModifiedBy { get; set; }
}

[ExcludeFromCodeCoverage]
public class BaseModel : IBaseModel
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public Guid Id { get; set; }

    public DateTime? DateCreated { get; set; }
    public Guid? CreatedBy { get; set; }
    public DateTime? DateLastModified { get; set; }
    public Guid? LastModifiedBy { get; set; }
}

as soon as you implement an interface like this you'll find all your Guid values are an empty guid.

As far as I could figure out (with some help from a colleague) its related to this post because basically the get_id() of the Guid property method is effectively now an interface method.

You need to add the addition check of !pi.GetGetMethod().IsFinal to distinguish between the interface method and the concrete class version. So my revised version of the solution from @trailmax looks like this:

public class IgnoreVirtualMembers : ISpecimenBuilder
{
    public object Create(object request, ISpecimenContext context)
    {
        if (context == null)
        {
            throw new ArgumentNullException("context");
        }

        var pi = request as PropertyInfo;
        if (pi == null)
        {
            return new NoSpecimen();
        }

        if (pi.GetGetMethod().IsVirtual && !pi.GetGetMethod().IsFinal)
        {
            return null;
        }
        return new NoSpecimen();
    }
}
Croton answered 5/5, 2023 at 10:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.