Модульное тестирование реактивного кода в конструкторах с использованием автоблокировки из автофиксации?

#c# #unit-testing #nunit #system.reactive #autofixture

Вопрос:

Я сталкиваюсь с небольшой проблемой с курицей и яйцом, настраивая свои насмешки для кода реактивных расширений в конструкторе. Вот тестируемый класс (вместе с интерфейсом службы, от которого он зависит):

 class MyViewModel
{
    public int Thing { get; set; }
    public MyViewModel(IMyService service)
    {
        service.StreamOfThings.Subscribe(x => Thing = x));
    }

    public void SomeClickEvent()
    {
        // Do something with `Thing`
    }
}
public interface IMyService
{
    IObservable<int> StreamOfThings { get; }
}
 

Чтобы упростить тестирование, я также определил пользовательский атрибут с именем AutoMockData , который я могу использовать для использования Moq для внедрения макетных экземпляров в мои классы с помощью автофиксации:

 public class AutoMockDataAttribute : AutoDataAttribute
{
    private static IFixture Create() =>
        new Fixture().Customize(new AutoMoqCustomization { ConfigureMembers = true });
        
    public AutoMockDataAttribute() : base(Create) {}
}
 

С этим я готов написать свой тест (используя NUnit3):

 [Test, AutoMockData]
public void Verify_some_behavior(
    [Frozen] Mock<IMyService> mockService,
    MyViewModel vm)
{
    mockService.Setup(x => x.StreamOfThings).Returns(Observable.Return(100));
    vm.SomeClickEvent();
    vm.Thing.Should().Be(100);
}
 

Этот тест не проходит. Я считаю, что это не удается, потому что конструктор MyViewModel устанавливает наблюдаемый конвейер на экземпляре IObservable , отличном от того, который я настроил в методе модульного тестирования.

Идеальным решением здесь было бы использовать IObservable<> экземпляр, настроенный с помощью автофиксации, но я не уверен, как это лучше всего сделать. Для этого каким-то образом нужно было бы уже создать для меня заранее построенный трубопровод.

Обходной путь, который я нашел, состоит в том, чтобы не использовать AutoMock функциональность AutoFixture и вместо этого создавать Fixture прямое и прямое использование методов .Freeze() и .Create() в правильном порядке. Однако это приводит к, возможно, более сложному для чтения и менее чистому корпусу модульного теста.

Как я могу продолжить реализацию своего теста, как показано здесь, но также иметь возможность настраивать любые наблюдаемые объекты до их использования в конструкторе SUT?

Ответ №1:

Я думаю, что проблема здесь в том, что вы никогда на самом деле не используете наблюдаемое, поэтому Thing сохраняете значение, которое автофиксация присвоила ему при создании.

В приведенном ниже примере Subject значение замораживается как IObservable<int> значение, которое затем разрешается как возвращаемое значение для StreamOfThings . Затем субъект вынужден вызвать подписчиков.

 [Test, AutoMockData]
public void Verify_some_behavior(
    [Frozen(Matching.ImplementedInterfaces)] Subject<int> observable, 
    MyViewModel vm)
{
    observable.OnNext(100);
    vm.SomeClickEvent();
    vm.Thing.Should().Be(100);
}
 

Этот пример эквивалентен следующему:

 [Test, AutoMockData]
public void Verify_some_behavior(
    Subject<int> observable,
    [Frozen] Mock<IMyService> mockService,
    MyViewModel vm)
{
    mockService.Setup(x => x.StreamOfThings).Returns(observable);

    observable.OnNext(100);
    vm.SomeClickEvent();

    vm.Thing.Should().Be(100);
}
 

Такой способ написания теста также должен сделать очевидным, как обрабатывать несколько наблюдаемых свойств.

Комментарии:

1. Я все еще изучаю автофиксацию. Это совершенно удивительно. Что вы делаете в ситуациях неопределенности? Например, если объект реализует несколько интерфейсов или у вас есть несколько IObservable<int> свойств?

2. В случае, если бы было несколько наблюдаемых объектов, я бы, вероятно, создал объекты с помощью автофиксации, а затем использовал бы API Moq для явного назначения наблюдаемых объектов. Моя рекомендация здесь состоит в том, чтобы начать с использования императивного API, предлагаемого AutoFixture, а затем искать способы оптимизации кода с помощью API декоратора (атрибута).

3. Когда один и тот же тип реализует несколько интерфейсов, вы можете ввести объект в качестве конкретной реализации интерфейса. Обычно это эквивалентно использованию атрибута FrozenAttribute с сопоставлением по реализованным интерфейсам или прямому базовому типу. fixture.Inject<IObservable<int>>(new Subject<int>()); Или вы можете полностью передать создание интерфейсов конкретным типам fixture.Customizations.Add(new TypeRelay(typeof(IObservable<>),typeof(Subject<>))); , если этого недостаточно, то, скорее всего, в вашем коде есть недостаток в дизайне.

4. Почему это не Subject должно быть заморожено во втором примере? На мой взгляд, mockService экземпляр будет построен с еще одной другой реализацией IObservable . Я ошибаюсь в этом? Если это так, то я не понимаю, почему там нет необходимости в замороженном виде.

5. Ах хорошо, значит, во втором случае вы просто эффективно используете автофиксацию в качестве исходного new для типа темы. Это интересно, я думаю, что на самом деле никогда не было никакой причины делать их вручную, вы можете просто бросить их в качестве аргумента. Спасибо!