Настройка входов конструктора для абстрактного класса в AutoFixture - PullRequest
0 голосов
/ 29 апреля 2020

При попытке модульного тестирования метода службы реализуемый им конструктор базового абстрактного класса содержит 2 параметра. Я хочу настроить эти параметры при вызове сервиса с помощью Auto-fixture.

Базовый код службы приведен ниже.

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);
   .
   .
}

Другая служба наследует от этого базового абстрактного класса

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

В моем модульном тесте я вызываю * Функция 1009 *, как показано ниже

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

AutoFixture пытается передать случайную строку параметру конструктора CronExpression в классе BaseService. Проблема в том, что CronExpression должен быть в указанном формате c, иначе при попытке его проанализировать возникает ошибка CronExpression.Parse(cronExpression)

Как передать пользовательское значение для параметров конструктора?

Заранее спасибо.

1 Ответ

2 голосов
/ 01 мая 2020

AutoFixture не знает об ограничениях домена для параметра string для вашего класса. Это должно дать подсказку, что, возможно, этот тип не самый подходящий параметр для класса. Это классический c пример Primitive Obsession .

. Вместо этого вы можете улучшить свой сервис, вместо этого напрямую принять тип, который воплощает идею, что это CronExpression:

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

Таким образом, гарантируется, что BaseService будет передан действительный CronExpression. Если это не так, то создание CronExpression уже будет неудачным.

Вы можете подумать, что мы просто переместили проблему: как вы теперь скажете AutoFixture создать действительный CronExpression?

Преимущество переноса работы на построение CronExpression заключается в том, что любая настройка, созданная для создания действительного CronExpression, теперь может использоваться повторно для любого другого типа, который потребует этого. Это уменьшит церемонию для любых будущих тестов, которые в конечном итоге будут нуждаться в этом типе. Не говоря уже о всех других преимуществах избегания примитивной одержимости.

Что касается сообщения AutoFixture о том, как создать действительный CronExpression, существует множество вариантов. Вы можете напрямую ввести один действительный:

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

Если вы хотите иметь возможность выбрать несколько, вы можете использовать ElementsBuilder для выбора из пула допустимых значений:

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

Наибольший контроль над созданием для этого типа можно получить, создав реализацию ISpecimenBuilder, которую я опущу в этом ответе. Если вы хотите go по этому маршруту, есть много примеров и здесь, и в других местах.

После того, как одна из этих настроек будет выполнена для AutoFixture, создание вашего sut будет работать:

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

Примечание: я не проверял, можно ли напрямую создать экземпляр TimeZoneInfo создателями образцов AutoFixture по умолчанию. Если это невозможно, аналогичный метод можно использовать и для этого типа.

...