#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
для типа темы. Это интересно, я думаю, что на самом деле никогда не было никакой причины делать их вручную, вы можете просто бросить их в качестве аргумента. Спасибо!