How to configure AutoFixture to use an enum value as seed when creating many of a certain type?
Asked Answered
R

5

9

I have the following types:

public enum Status
{
    Online,
    Offline
}

public class User
{
    private readonly Status _status;
    public User(Status status) { _status = status; }
    public Status Status {get {return _status; }}
    public string Name {get;set;}
}

Now, when executing fixture.CreateMany<User> I want AutoFixture to return two Users, one per status. All other properties - like Name - should be filled with anonymous data.

Question:
How to configure AutoFixture to do this?


I tried the following this:

  1. Register collection that news up the User object:

    fixture.Register(
        () => Enum.GetValues(typeof(Status)).Cast<Status>().Select(s => 
            new User(s)));
    

    The problem with this approach is that AutoFixture doesn't fill the other properties like Name

  2. Customize User to use a factory and register a collection that uses fixture.Create:

        f.Customize<User>(c => c.FromFactory((Status s) => new User(s)));
        f.Register(() =>
            Enum.GetValues(typeof(Status))
                .Cast<Status>()
                .Select(s => (User)f.Create(new SeededRequest(typeof(User), s),
                                            new SpecimenContext(f))));
    

    That didn't work either. The seed isn't being used.

Ruination answered 14/6, 2013 at 15:15 Comment(0)
C
6

You could do this:

var users = new Fixture().Create<Generator<User>>();

var onlineUser = users.Where(u => u.Status == Status.Online).First();
var offlineUser = users.Where(u => u.Status == Status.Offline).First();

If you're using AutoFixture.Xunit, the declarative equivalent is:

[Theory, AutoData]
public void CreateOneOfEachDeclaratively(Generator<User> users)
{
    var onlineUser = users.Where(u => u.Status == Status.Online).First();
    var offlineUser = users.Where(u => u.Status == Status.Offline).First();

    // Use onlineUser and offlineUser here...
}
Catha answered 18/6, 2013 at 13:23 Comment(10)
Thanks for your answer. Unfortunatelly, that doesn't work if the Status property isn't readonly. First "never" returns (I waited at least 30 seconds).Ruination
The readonly keyword has nothing to do with it. I just removed the readonly keyword from my repro, and it didn't change the result.Catha
I am not talking about the backing field not being readonly. I am talking about the property not being readonly, i.e. public Status Status { get; set; }.Ruination
OK, I can reproduce this if I make the property writable, and keep the constructor argument. What you're seeing here is a resonance effect, because each instance receives exactly two instances of Status, and because there's only two values of Status, it'll always end up being the same because enum values are (still) generated in a deterministic round-robin fashion. Perhaps we should change the default behaviour of AutoFixture, but that would be a breaking change... If you can make the property read-only, remove the constructor argument, or use a larger enum, you should be good.Catha
Thanks for the explanation - that makes sense. Another way to "fix" it is by customizing User with c => c.Without(x => x.Status). That's the cleanest solution IMO, because it doesn't touch the User class itself which (hopefully) has good reasons to be designed the way it is. (Please note: User was just an example for the sake of this question. A discussion whether or not this particular design is good or not is not necessary).Ruination
Now that I think about it, if the properties are writable, a much easier solution is to just create two instances of User and then assign the values subsequently. No AutoFixture tweaking would be required at all.Catha
I don't understand that comment. Can you elaborate? Please keep in mind that I don't care for the distinct instances of User, I only care for the IEnumerable<User>. And it should contain one user per enum value, no matter how many enum values there are.Ruination
var users = fixture.Create<Generator<User>>().Take(2).ToList(); gives you two instances of User. If Status is writable, you can just go users[0].Status = Status.Online; and users[1].Status = Status.Offline;Catha
Yeah, but that's what I want to avoid doing in the first place, because it would mean that I would have to do it in every single unit test that requires an IEnumerable<User>. See my answer to see what I am doing now to avoid this.Ruination
Something like that was going to be my next suggestion :)Catha
S
6

You may declare and use a customization, e.g. StatusGenerator:

var fixture = new Fixture();
fixture.RepeatCount = 2;
fixture.Customizations.Add(new StatusGenerator());

var result = fixture.CreateMany<User>();

A hypothetical implementation of the StatusGenerator could be the following:

internal class StatusGenerator : ISpecimenBuilder
{
    private readonly Status[] values;
    private int i;

    internal StatusGenerator()
    {
        this.values =
            Enum.GetValues(typeof(Status)).Cast<Status>().ToArray();
    }

    public object Create(object request, ISpecimenContext context)
    {
        var pi = request as ParameterInfo;
        if (pi == null || !pi.ParameterType.IsEnum)
            return new NoSpecimen(request);

        return this.values[i == this.values.Length - 1 ? i = 0 : ++i];
    }
}
Sims answered 14/6, 2013 at 20:52 Comment(1)
Nice, thanks. Would it be possible without setting RepeatCount? As I understand, this affects all calls to CreateMany, not just those for User...Ruination
C
6

You could do this:

var users = new Fixture().Create<Generator<User>>();

var onlineUser = users.Where(u => u.Status == Status.Online).First();
var offlineUser = users.Where(u => u.Status == Status.Offline).First();

If you're using AutoFixture.Xunit, the declarative equivalent is:

[Theory, AutoData]
public void CreateOneOfEachDeclaratively(Generator<User> users)
{
    var onlineUser = users.Where(u => u.Status == Status.Online).First();
    var offlineUser = users.Where(u => u.Status == Status.Offline).First();

    // Use onlineUser and offlineUser here...
}
Catha answered 18/6, 2013 at 13:23 Comment(10)
Thanks for your answer. Unfortunatelly, that doesn't work if the Status property isn't readonly. First "never" returns (I waited at least 30 seconds).Ruination
The readonly keyword has nothing to do with it. I just removed the readonly keyword from my repro, and it didn't change the result.Catha
I am not talking about the backing field not being readonly. I am talking about the property not being readonly, i.e. public Status Status { get; set; }.Ruination
OK, I can reproduce this if I make the property writable, and keep the constructor argument. What you're seeing here is a resonance effect, because each instance receives exactly two instances of Status, and because there's only two values of Status, it'll always end up being the same because enum values are (still) generated in a deterministic round-robin fashion. Perhaps we should change the default behaviour of AutoFixture, but that would be a breaking change... If you can make the property read-only, remove the constructor argument, or use a larger enum, you should be good.Catha
Thanks for the explanation - that makes sense. Another way to "fix" it is by customizing User with c => c.Without(x => x.Status). That's the cleanest solution IMO, because it doesn't touch the User class itself which (hopefully) has good reasons to be designed the way it is. (Please note: User was just an example for the sake of this question. A discussion whether or not this particular design is good or not is not necessary).Ruination
Now that I think about it, if the properties are writable, a much easier solution is to just create two instances of User and then assign the values subsequently. No AutoFixture tweaking would be required at all.Catha
I don't understand that comment. Can you elaborate? Please keep in mind that I don't care for the distinct instances of User, I only care for the IEnumerable<User>. And it should contain one user per enum value, no matter how many enum values there are.Ruination
var users = fixture.Create<Generator<User>>().Take(2).ToList(); gives you two instances of User. If Status is writable, you can just go users[0].Status = Status.Online; and users[1].Status = Status.Offline;Catha
Yeah, but that's what I want to avoid doing in the first place, because it would mean that I would have to do it in every single unit test that requires an IEnumerable<User>. See my answer to see what I am doing now to avoid this.Ruination
Something like that was going to be my next suggestion :)Catha
R
4

Based on Mark's answer, this is what I am using now:

fixture.Customize<User>(c => c.Without(x => x.Status));
fixture.Customize<IEnumerable<User>>(
    c =>
    c.FromFactory(
        () => Enum.GetValues(typeof(Status)).Cast<Status>()
                  .Select(s => users.First(u => u.Status == s))));

fixture.Create<IEnumerable<User>>(); // returns two Users
Ruination answered 25/6, 2013 at 15:43 Comment(0)
P
2

I know it is already answered and the Generator was a very interesting finding. I think there is a much simpler approach for this problem.

        var numberOfEnumValues = Enum.GetValues(typeof(Status)).Length;
        var users = fixture.CreateMany<User>(numberOfEnumValues);

In case the constructor is more complicated, with multiple Status values, or the model has property setters of Status type. Then you generally have a problem, and the generator might blow as well.

Say that:

    public class SuperUser : User
    {
        public SuperUser(Status status, Status shownStatus): base(status)
        {
        }
    }

Then this will never be evaluated:

    var users = fixture.Create<Generator<SuperUser>>();
    var offlineUser = users.Where(u => u.Status == Status.Offline).First();
Persecute answered 30/7, 2013 at 21:39 Comment(0)
K
0

Current way of doing this with AutoFixture 4.17.0

fixture
.Build<User>()
.With(u => u.Status, Status.Offline)
.CreateMany(5)
.ToList();
Kernite answered 9/12, 2022 at 10:50 Comment(1)
Are you sure? To me, it looks like it will create 5 users all with the Status being Offline. That's not what I asked 9 years agoRuination

© 2022 - 2024 — McMap. All rights reserved.