Search code examples
c#.net-coreabstract-classmstestautofixture

Customising constructor inputs to an abstract class in AutoFixture


While trying to unit test a service method, the base abstract class constructor that it implements contains 2 parameters. I want to customise these parameters while invoking the service using Auto-fixture.

The base service code is as below.

public abstract class ServiceBase : IHostedService, IDisposable
{
   private readonly CronExpression _expression;
   private readonly TimeZoneInfo _timeZoneInfo;

   protected BaseService(string cronExpression, TimeZoneInfo timeZoneInfo)
   {
      _expression = CronExpression.Parse(cronExpression);
      _timeZoneInfo = timeZoneInfo;
   }

   public abstract Task ExecuteTask(CancellationToken cancellationToken);
   .
   .
}

Another Service inherits from this base abstract class

public class TestService : ServiceBase
{
   public override async Task ExecuteTask(CancellationToken stoppingToken)
   {
      //Implementation here
   }
}

In my Unit test I'm invoking the ExecuteTask function as below

Func<Task> executeAction = async () => await sut.ExecuteTask(A<CancellationToken>._);
executeAction.Should().NotThrow();

AutoFixture tries to pass a random string to the CronExpression constructor parameter in BaseService class. Problem is this CronExpression has to be in a specific format or else an error occurs while trying to Parse it CronExpression.Parse(cronExpression)

How can i pass a custom value for constructor parameters?

Thanks in advance.


Solution

  • AutoFixture doesn't know about the domain constraints for the string parameter to your class. This should give a hint that, perhaps this type isn't the most appropriate parameter to the class. This is a classic example of Primitive Obsession.

    You might enhance your service to, instead, directly accept the type that encapsulates the idea that this is a CronExpression:

    protected ServiceBase(CronExpression cronExpression, TimeZoneInfo timeZoneInfo)
    {
        _cronExpression = cronExpression;
        ...
    }
    

    This way, it's guaranteed that the BaseService will be passed a valid CronExpression. If it wasn't, then creation of the CronExpression will have already failed.

    You might think that we've simply shifted the problem: How do you now tell AutoFixture to create a valid CronExpression?

    The advantage of shifting the work to building a CronExpression is that any customization made to create a valid CronExpression is now reusable for any other type that will require it. This will reduce ceremony for any future tests that will end up needing that type. Not to mention all the other benefits of avoiding primitive obsession.

    Regarding telling AutoFixture how to create a valid CronExpression, there are many options. You might directly inject a single valid one:

    fixture.Inject(CronExpression.Parse("myValidCronExpression"));
    
    

    If you want to be able to select multiple, you could use ElementsBuilder to select from a pool of valid values:

    public void Test()
    {
        var fixture = new Fixture();
        fixture.Customizations.Add(new ElementsBuilder<CronExpression>(
            new[] { "validExpr1", "validExpr2" }
            .Select(s => CronExpression.Parse(s))
            .ToArray()))
        ...
    }
    

    The most control of creation for that type would be afforded by creating an implementation of ISpecimenBuilder, which I'll omit in this answer. If you want to go that route, there are many examples both here and elsewhere.

    After one of those customizations is made to AutoFixture, creation of your sut will work:

    var sut = fixture.Create<TestService>();
    

    Note: I haven't tested if TimeZoneInfo can directly be instantiated by AutoFixture's default specimen builders. If it can't, a similar method can be used for that type as well.