如何解决NUnit测试依赖密封类的ParentViewModel问题?
如何测试依赖密封类的ViewModel?
是不是设计问题?
不算严重的设计缺陷,但当前ParentViewModel直接依赖密封的具体ViewModel类,属于紧耦合设计,导致测试时无法通过常规Mock工具隔离依赖,这是设计灵活性不足的表现,但还没到“坏设计”的程度,只是有优化空间。
可行的解决方案
方案1:使用真实的ViewModel实例而非Mock
如果AViewModel和BViewModel的逻辑不复杂,或者可以通过构造参数、设置内部状态(比如调整输入数据)来控制它们的HasBeenFilled、PropertyA/PropertyB返回值,直接在测试中实例化真实对象即可。
示例代码:
public void Test_HasBeenFilled() { // 假设AViewModel可通过构造参数控制HasBeenFilled返回值 var realA = new AViewModel(inputThatMakesFilledTrue); var realB = new BViewModel(inputThatMakesFilledFalse); var viewModel = new ParentViewModel(realA, realB); Assert.True(viewModel.HasBeenFilled); }
- 优点:完全不用修改生产代码,避免为了测试改设计。
- 缺点:如果A/B的依赖复杂,初始化真实实例需要准备大量测试数据,测试会变得繁琐。
方案2:使用支持密封类Mock的测试工具
常规Mock工具(如Moq)无法Mock密封类,但部分工具支持这类场景:
- JustMock:商业工具,支持Mock密封类、静态方法等非虚成员。
- Pose:开源工具,通过IL拦截实现对密封类成员的Mock。
以Pose为例的示例代码:
public void Test_HasBeenFilled() { // 拦截AViewModel的HasBeenFilled属性,返回true var mockA = Shim.Replace(() => new AViewModel().HasBeenFilled).With(() => true); // 拦截BViewModel的HasBeenFilled属性,返回false var mockB = Shim.Replace(() => new BViewModel().HasBeenFilled).With(() => false); using (ShimContext.Create()) { var viewModel = new ParentViewModel(new AViewModel(), new BViewModel()); Assert.True(viewModel.HasBeenFilled); } }
- 优点:不用修改生产代码,直接Mock密封类成员。
- 缺点:需要引入额外依赖,部分工具是收费的,且IL拦截可能有性能或兼容性问题。
方案3:提取基于行为的接口(推荐长期方案)
不要给每个ViewModel加专属接口,而是根据ParentViewModel需要的行为提取接口,让密封类实现这些接口,然后ParentViewModel依赖接口而非具体类。
比如针对Parent的两个需求:
- 需要判断子ViewModel是否已填充 → 提取
IHasFilledState接口 - 需要验证子ViewModel的特定属性 → 提取对应业务语义的接口(比如
IValidatableRuleA、IValidatableRuleB)
代码示例:
// 基于行为的接口 public interface IHasFilledState { bool HasBeenFilled { get; } } public interface IValidatableRuleA { bool PropertyA { get; } } public interface IValidatableRuleB { bool PropertyB { get; } } // 密封类实现接口 public sealed class AViewModel : IHasFilledState, IValidatableRuleA { public bool HasBeenFilled => /* 原有逻辑 */; public bool PropertyA => /* 原有逻辑 */; } public sealed class BViewModel : IHasFilledState, IValidatableRuleB { public bool HasBeenFilled => /* 原有逻辑 */; public bool PropertyB => /* 原有逻辑 */; } // ParentViewModel依赖接口 public class ParentViewModel { private readonly IHasFilledState _aFilledState; private readonly IHasFilledState _bFilledState; private readonly IValidatableRuleA _aRule; private readonly IValidatableRuleB _bRule; public ParentViewModel(IHasFilledState aFilledState, IHasFilledState bFilledState, IValidatableRuleA aRule, IValidatableRuleB bRule) { _aFilledState = aFilledState; _bFilledState = bFilledState; _aRule = aRule; _bRule = bRule; } public bool HasBeenFilled => _aFilledState.HasBeenFilled || _bFilledState.HasBeenFilled; public bool AnotherMethod => _aRule.PropertyA && _bRule.PropertyB; }
或者更简洁的,把每个子ViewModel的行为合并成一个接口:
public interface IChildViewModelA : IHasFilledState, IValidatableRuleA { } public interface IChildViewModelB : IHasFilledState, IValidatableRuleB { } public sealed class AViewModel : IChildViewModelA { /* 实现 */ } public sealed class BViewModel : IChildViewModelB { /* 实现 */ } public ParentViewModel(IChildViewModelA a, IChildViewModelB b) { // 赋值逻辑 }
- 优点:解耦了Parent和具体类,测试时可以轻松Mock接口;同时接口是基于业务行为定义的,不是为了测试强行添加,反而让设计更清晰,符合依赖倒置原则。
- 缺点:需要修改生产代码,但这是对设计的合理优化,不是“测试驱动过度设计”。
总结
- 如果只想快速解决测试问题,优先考虑用真实实例或更换Mock工具;
- 如果希望代码长期可维护,提取基于行为的接口是最优解,这不是过度设计,而是弥补紧耦合设计的不足。
内容的提问来源于stack exchange,提问作者Massimo
相关产品推荐
相关产品推荐

