使用 AutoFixture 中的 AutoMock 对构造函数中的反应式代码进行单元测试?
Unit testing Reactive code in constructors using AutoMock from AutoFixture?
我在构造函数中为 Reactive Extensions 代码设置模拟时遇到了一些先有鸡还是先有蛋的问题。这是正在测试的class(连同它所依赖的服务接口):
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 通过 AutoFixture 将模拟实例注入我的 classes:
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
的不同实例上设置了一个可观察管道,而不是我在单元测试方法中设置的实例。
此处理想的解决方案是使用使用 AutoFixture 设置的 IObservable<>
实例,但我不确定如何最好地做到这一点。它会以某种方式需要已经为我设置了一个预构建的管道。
我找到的解决方法是不使用 AutoFixture
的 AutoMock
功能,而是直接构造一个 Fixture
并直接使用 .Freeze()
和 .Create()
方法的正确顺序。然而,这可能会导致更难阅读且更不干净的单元测试主体。
我如何才能继续执行此处所示的测试,同时还能在 SUT 的构造函数中使用之前设置任何可观察对象?
我认为这里的问题是您从未实际使用可观察对象,因此 Thing
保留 AutoFixture 在创建时分配给它的值。
在下面的示例中,Subject
被冻结为 IObservable<int>
,然后解析为 StreamOfThings
的 return 值。然后主题强制触发订阅者。
[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);
}
这种编写测试的方式也应该使如何处理多个可观察属性变得显而易见。
我在构造函数中为 Reactive Extensions 代码设置模拟时遇到了一些先有鸡还是先有蛋的问题。这是正在测试的class(连同它所依赖的服务接口):
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 通过 AutoFixture 将模拟实例注入我的 classes:
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
的不同实例上设置了一个可观察管道,而不是我在单元测试方法中设置的实例。
此处理想的解决方案是使用使用 AutoFixture 设置的 IObservable<>
实例,但我不确定如何最好地做到这一点。它会以某种方式需要已经为我设置了一个预构建的管道。
我找到的解决方法是不使用 AutoFixture
的 AutoMock
功能,而是直接构造一个 Fixture
并直接使用 .Freeze()
和 .Create()
方法的正确顺序。然而,这可能会导致更难阅读且更不干净的单元测试主体。
我如何才能继续执行此处所示的测试,同时还能在 SUT 的构造函数中使用之前设置任何可观察对象?
我认为这里的问题是您从未实际使用可观察对象,因此 Thing
保留 AutoFixture 在创建时分配给它的值。
在下面的示例中,Subject
被冻结为 IObservable<int>
,然后解析为 StreamOfThings
的 return 值。然后主题强制触发订阅者。
[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);
}
这种编写测试的方式也应该使如何处理多个可观察属性变得显而易见。