9

My test requires that I set the Response property on an immutable Rsvp object (see below) to a specific value.

public class Rsvp
{
    public string Response { get; private set; }

    public Rsvp(string response)
    {
        Response = response;
    }
}

I initially tried to do this using Build<Rsvp>().With(x => x.Rsvp, "Attending"), but realized this only supports writable properties.

I replaced that with Build<Rsvp>().FromFactory(new Rsvp("Attending")). This works, but is cumbersome for more complex objects where it doesn't matter what some of the properties are.

For instance, if the Rsvp object had a CreatedDate property, this method of instantiating the object would force me to write Build<Rsvp>().FromFactory(new Rsvp("Attending", fixture.Create<DateTime>())).

Is there a way to only specify values for meaning properties for an immutable object?

Ruben Bartelink
  • 59,778
  • 26
  • 187
  • 249
Matt Slavicek
  • 841
  • 10
  • 18

3 Answers3

10

AutoFixture was originally build as a tool for Test-Driven Development (TDD), and TDD is all about feedback. In the spirit of GOOS, you should listen to your tests. If the tests are hard to write, you should consider your API design. AutoFixture tends to amplify that sort of feedback.

Frankly, immutable types are a pain in C#, but you can make it easier to work with a class like Rsvp if you take a cue from F# and introduce copy and update semantics. If you modify Rsvp like this, it's going to be much easier to work with overall, and thus, as a by-product, also to unit test:

public class Rsvp
{
    public string Response { get; private set; }

    public DateTime CreatedDate { get; private set; }

    public Rsvp(string response, DateTime createdDate)
    {
        Response = response;
        CreatedDate = createdDate;
    }

    public Rsvp WithResponse(string newResponse)
    {
        return new Rsvp(newResponse, this.CreatedDate);
    }

    public Rsvp WithCreatedDate(DateTime newCreatedDate)
    {
        return new Rsvp(this.Response, newCreatedDate);
    }
}

Notice that I've add two WithXyz methods, that return a new instance with that one value changed, but all other values held constant.

This would enable you to create an instance of Rsvp for testing purposed like this:

var fixture = new Fixture();
var seed = fixture.Create<Rsvp>();
var sut = seed.WithResponse("Attending");

or, as a one-liner:

var sut = new Fixture().Create<Rsvp>().WithResponse("Attending");

If you can't change Rsvp, you can add the WithXyz methods as extension methods.

Once you've done this about a dozen times, you get tired of it, and it's time to make the move to F#, where all that (and more) is built-in:

type Rsvp = {
    Response : string
    CreatedDate : DateTime }

You can create an Rsvp record with AutoFixture like this:

let fixture = Fixture()
let seed = fixture.Create<Rsvp>()
let sut = { seed with Response = "Attending" }

or, as a one-liner:

let sut = { Fixture().Create<Rsvp>() with Response = "Attending" }
Mark Seemann
  • 225,310
  • 48
  • 427
  • 736
  • 2
    +1 If @mattslav can modify the `Rsvp` class (or add the extension methods) then this should be the accepted answer. – Nikos Baxevanis Dec 29 '13 at 12:26
  • 4
    The first paragraph of this answer is so well written that we could actually include it in description at AutoFixture's README file. – Nikos Baxevanis Dec 29 '13 at 12:28
  • With records in F# you loose the ability to define invariants on the properties. You might want to limit the shape of a string property to a subset of all strings. This is where classes shine. – fsl Dec 30 '13 at 09:26
  • 1
    I wouldn't say the tests are hard to write. I wasn't sure the best way to implement the Object Builder pattern using AutoFixture as the engine for creating objects. Using extension methods that are only available to tests seems to be the best option, as this is something that only tests need to be able to do. – Matt Slavicek Dec 30 '13 at 13:27
  • @mattslav Sure you can add it as test-only code, but IME, it's a valuable feature to have in production code too. The benefit of TDD is that it gives you feedback about the *production* API of your code. – Mark Seemann Dec 30 '13 at 14:50
  • @MarkSeemann In your `Rsvp` implementation, it looks like you have the same capabilities (including chaining) as if you'd implemented a nested `RsvpBuilder` class that had `WithXyz()` methods. Does the nested builder do nothing more, then, than to encapsulate the creation logic, and (if the `Rsvp` constructors are made private) ensure its status as 'sole entry point' into `Rsvp` construction? – Jeff Jan 18 '14 at 20:46
  • @MarkSeemann Also, by keeping the `Rsvp` constructor public, while preserving immutability, is this what permits `Autofixture` to still create these immutable objects for us without complaining about '...no public constructor, is an abstract or non-public type...'? – Jeff Jan 18 '14 at 20:52
  • @Lumirris With a *copy-and-update* API, the class becomes its own *Builder*, so that you don't need a separate Builder. I much prefer this pattern, as it's more succinct. FWIW, I stole the idea from F#. Keeping the constructor (and the class itself) public means that it's just a 'normal' concrete class, and AutoFixture deals with those without further needs for extensions or customizations. It's poka-yoke design: http://blog.ploeh.dk/2011/05/24/Poka-yokeDesignFromSmelltoFragrance/ – Mark Seemann Jan 18 '14 at 22:03
  • @MarkSeemann In a scenario in which object instantiation is expensive, doesn't this approach become less desirable as more calls to `WithAbc`, ... `WithXyz` are made (and new instances created with each call?). Or is this simply a balance that must be struck when one needs immutable classes? – Jeff Jan 18 '14 at 23:29
  • @MarkSeemann I'm familiar with your [Poka-yoke](http://blog.ploeh.dk/2011/05/24/Poka-yokeDesignFromSmelltoFragrance/) blog series; which of the 5 parts of the series specifically deals with the issue here (if any) - I couldn't figure that out. – Jeff Jan 18 '14 at 23:31
  • @Lumirris What sort of scenario is it where object instantiation is expensive? – Mark Seemann Jan 19 '14 at 08:03
  • @MarkSeemann Heh heh. I guess you called my bluff, as I can't think of any specific example, but... *aren't* there scenarios in which it would be expensive? I guess something just *felt* wrong to me seeing new instances being created for each chained `WithXyz` call, when many calls are made... – Jeff Jan 19 '14 at 19:49
  • @Lumirris Unless you go out of your way to *make* instantiation expensive, it's not. http://blog.ploeh.dk/2011/03/04/Composeobjectgraphswithconfidence – Mark Seemann Jan 19 '14 at 20:55
  • While I agree that F# is a better way to go, unfortunately my group isn't willing to have production F# code yet, so I'm stuck with C#. However, I _have_ gotten buy-in of immutable types. When testing with these, it can be very handy to have an anonymous variable with a specific property set to a specific value. For example, when using a **Parameter Object** I may want to write a test for when `x.Foo == True` and another for when `x.Foo == False`, but this is a pain if there are a lot of properties on `x`. In this case we don't need production copy-and-update code for `x`, only for testing. – Matt Klein Sep 02 '16 at 20:28
4

As long as the Response property is readonly*, you can define a custom SpecimenBuilder for the Rsvp type:

internal class RsvpBuilder : ISpecimenBuilder
{
    public object Create(object request, ISpecimenContext context)
    {
        var pi = request as ParameterInfo;
        if (pi == null)
            return new NoSpecimen();

        if (pi.ParameterType != typeof(string) || pi.Name != "response")
            return new NoSpecimen();

        return "Attending";
    }
}

The following test passes:

[Fact]
public void ResponseIsCorrect()
{
    var fixture = new Fixture();
    fixture.Customizations.Add(new RsvpBuilder());
    var sut = fixture.Create<Rsvp>();

    var actual = sut.Response;

    Assert.Equal("Attending", actual);
}

* If for some reason the Response property becomes writable you can follow the solution in this answer.

Community
  • 1
  • 1
Nikos Baxevanis
  • 10,868
  • 2
  • 46
  • 80
  • I was hoping for a solution that didn't require an object builder to be created for every object. Additionally, the test would need to pass the `Response` value to the `RsvpBuilder`, how would it do that? – Matt Slavicek Dec 28 '13 at 13:40
  • 1
    @mattslav Is is possible as Mark Seemann [answered](http://stackoverflow.com/a/20816487/467754). – Nikos Baxevanis Dec 29 '13 at 12:32
2

Extending Nikos´ answer, we can generalize the customization to work with any property as such:

public class OverridePropertyBuilder<T, TProp> : ISpecimenBuilder
{
    private readonly PropertyInfo _propertyInfo;
    private readonly TProp _value;

    public OverridePropertyBuilder(Expression<Func<T, TProp>> expr, TProp value)
    {
        _propertyInfo = (expr.Body as MemberExpression)?.Member as PropertyInfo ??
                        throw new InvalidOperationException("invalid property expression");
        _value = value;
    }

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

        var camelCase = Regex.Replace(_propertyInfo.Name, @"(\w)(.*)",
            m => m.Groups[1].Value.ToLower() + m.Groups[2]);

        if (pi.ParameterType != typeof(TProp) || pi.Name != camelCase)
            return new NoSpecimen();

        return _value;
    }
}

But then we need custom extension methods to make it easier to use:

public class FixtureCustomization<T>
{
    public Fixture Fixture { get; }

    public FixtureCustomization(Fixture fixture)
    {
        Fixture = fixture;
    }

    public FixtureCustomization<T> With<TProp>(Expression<Func<T, TProp>> expr, TProp value)
    {
        Fixture.Customizations.Add(new OverridePropertyBuilder<T, TProp>(expr, value));
        return this;
    }

    public T Create() => Fixture.Create<T>();
}

public static class CompositionExt
{
    public static FixtureCustomization<T> For<T>(this Fixture fixture)
        => new FixtureCustomization<T>(fixture);
}

we then use it in your example as:

var obj = 
  new Fixture()
    .For<Rsvp>()
    .With(x => x.Response, "Attending")
    .Create();
Fabio Marreco
  • 2,186
  • 2
  • 23
  • 24
  • I've tried this, `pi.Name != camelCase` is always true, because `pi.Name` is _always_ "value". Any idea why that is? – user1007074 Apr 04 '22 at 14:35