Search code examples
c#moqxunit.netautofixture

How do I use AutoFixture Attributes to get FakeTimeProvider reference when my SUT depends on TimeProvider


Say I have a Service that has a dependency on TimeProvider.

What I do now to setup the use of FakeTimeProvider for my tests using AutoFixture and Moq is:

public class FakeTimeProviderCustomization : ICustomization {
    public void Customize(IFixture fixture) {
        fixture.Customize<TimeProvider>
            (c => c.FromFactory(() => new FakeTimeProvider()));
    }
}

public class MoqAutoDataAttribute : AutoDataAttribute {
    public MoqAutoDataAttribute()
        : base(() => new Fixture()
        .Customize(new AutoMoqCustomization())
        .Customize(new FakeTimeProviderCustomization())
        ) {
    }
}

[Theory, MoqAutoData]
public async Task Test_TimeRelatedStuff(
    [Frozen] TimeProvider timeProvider,
    Service sut) {
    
    ...
    var fakeTimeProvider = timeProvider as FakeTimeProvider;
    ...

Can I avoid the cast to FakeTimeProvider that I currently have to make each time?


Solution

  • You can use the Matching API provided by the [Frozen] attribute, like shown below, but that requires that you deactivate FakeTimeProviderCustomization.

    This test passes:

    [Theory, MoqAutoData]
    public async Task Test_TimeRelatedStuff(
        [Frozen(Matching.DirectBaseType)] FakeTimeProvider fakeTimeProvider,
        Service sut)
    {
    
        //... Cast is no longer necessary
        //var fakeTimeProvider = timeProvider as FakeTimeProvider;
        //...
        Assert.Equal(fakeTimeProvider, sut.TimeProvider);
    }
    

    where I've defined Service like this for demonstration purposes:

    public sealed class Service(TimeProvider timeProvider)
    {
        public TimeProvider TimeProvider { get; } = timeProvider;
    }
    

    As already underlined, the Matching.DirectBaseType criterion doesn't play well with the Customize API, in the sense that the latter overrides the former. To make the above test pass, I deactivated the FakeTimeProviderCustomization:

    public class FakeTimeProviderCustomization : ICustomization
    {
        public void Customize(IFixture fixture)
        {
            //fixture.Customize<TimeProvider>
            //    (c => c.FromFactory(() => new FakeTimeProvider()));
        }
    }
    

    Obviously, this renders the class ineffective, so you might as well delete it. I'm only showing the above here in order to explicitly explain how to make the test pass.


    Alternatively, if you don't want to deactivate FakeTimeProviderCustomization, you can use another matching rule. If your Service constructor looks like Service(TimeProvider timeProvider), you can alternatively write the test like this:

    [Theory, MoqAutoData]
    public async Task Test_TimeRelatedStuff(
        [Frozen(Matching.ParameterName)] FakeTimeProvider timeProvider,
        Service sut)
    {
    
        //... Cast is no longer necessary
        //var fakeTimeProvider = timeProvider as FakeTimeProvider;
        //...
        Assert.Equal(timeProvider, sut.TimeProvider);
    }
    

    Notice that the test parameter name timeProvider is the same as the dependency name for Service. This works even if you keep FakeTimeProviderCustomization active for other reasons.

    Obviously, as these provisos suggest, both of these 'solutions' are brittle, so you best course of action might be to reconsider the overall API design and test strategy.