无法修改外部Item类时,如何测试Data类DoWork是否调用Update方法?
解决方案
要验证Data.DoWork()是否正确调用了Item1.Update("Q")和Item2.Update("W"),同时避免耦合Item类的内部实现,分两种场景给出方案:
场景1:允许修改Data类(推荐,符合依赖倒置原则)
这种方式通过抽象解耦,是长期维护的最优解:
定义抽象接口:
创建一个包含Update方法的接口,统一Item1和Item2的行为:public interface IUpdatable { void Update(string value); }实现适配器类:
为外部的Item1和Item2编写适配器,实现IUpdatable接口,转发调用到原类:public class Item1Adapter : IUpdatable { private readonly Item1 _item; public Item1Adapter(Item1 item) => _item = item; public void Update(string value) => _item.Update(value); } public class Item2Adapter : IUpdatable { private readonly Item2 _item; public Item2Adapter(Item2 item) => _item = item; public void Update(string value) => _item.Update(value); }修改
Data类依赖抽象:
将Data类中的具体类型替换为接口,支持依赖注入:public class Data { public IUpdatable Item1 { get; set; } public IUpdatable Item2 { get; set; } public void Dowork() { Item1.Update("Q"); Item2.Update("W"); } }编写测试用例:
使用Moq等开源模拟框架,验证接口方法的调用情况:[TestClass] public class DataTests { [TestMethod] public void DoWork_CallsUpdateWithCorrectParameters() { // 准备模拟对象 var mockItem1 = new Mock<IUpdatable>(); var mockItem2 = new Mock<IUpdatable>(); var data = new Data { Item1 = mockItem1.Object, Item2 = mockItem2.Object }; // 执行方法 data.Dowork(); // 验证调用:检查Update是否被调用一次,参数为"Q"和"W" mockItem1.Verify(x => x.Update("Q"), Times.Once); mockItem2.Verify(x => x.Update("W"), Times.Once); } }
场景2:无法修改Data类(完全依赖外部类)
如果不能调整Data的依赖结构,可通过以下方式实现无耦合测试:
方法A:使用支持非虚方法模拟的商业框架
像TypeMock Isolator或Telerik JustMock这类框架可以直接拦截非虚方法调用,无需修改原代码:
[Test] public void DoWork_VerifiesUpdateCalls() { // 创建Item1和Item2的模拟实例 var mockItem1 = Mock.Create<Item1>(); var mockItem2 = Mock.Create<Item2>(); var data = new Data { Item1 = mockItem1, Item2 = mockItem2 }; // 执行测试方法 data.Dowork(); // 验证Update方法的调用次数和参数 Mock.Assert(() => mockItem1.Update("Q"), Occurs.Once()); Mock.Assert(() => mockItem2.Update("W"), Occurs.Once()); }
方法B:用Castle DynamicProxy创建动态代理(开源方案)
利用Castle.Core的动态代理功能,拦截非虚方法调用并记录状态:
安装NuGet包
Castle.Core编写拦截器类,记录调用情况:
public class UpdateCallTracker : IInterceptor { public bool UpdateCalled { get; private set; } public string LastUpdateValue { get; private set; } public void Intercept(IInvocation invocation) { if (invocation.Method.Name == nameof(Item1.Update)) { UpdateCalled = true; LastUpdateValue = invocation.Arguments[0] as string; } // 可选:调用原方法,若不需要验证Item内部逻辑可跳过 invocation.Proceed(); } }编写测试用例:
[TestMethod] public void DoWork_RecordsUpdateCalls() { // 创建代理生成器和拦截器 var proxyGenerator = new ProxyGenerator(); var tracker1 = new UpdateCallTracker(); var tracker2 = new UpdateCallTracker(); // 生成Item1和Item2的代理实例 var item1Proxy = proxyGenerator.CreateClassProxy<Item1>(tracker1); var item2Proxy = proxyGenerator.CreateClassProxy<Item2>(tracker2); var data = new Data { Item1 = item1Proxy, Item2 = item2Proxy }; // 执行方法 data.Dowork(); // 验证调用状态 Assert.IsTrue(tracker1.UpdateCalled); Assert.AreEqual("Q", tracker1.LastUpdateValue); Assert.IsTrue(tracker2.UpdateCalled); Assert.AreEqual("W", tracker2.LastUpdateValue); }
注意事项
- 如果
Item1或Item2是密封类(sealed),动态代理方案会失效,此时只能选择商业框架或更底层的方法注入(如Fody织入)。 - 优先选择场景1的抽象解耦方案,这能让代码更易维护,测试更稳定。
内容的提问来源于stack exchange,提问作者Bert Terb
相关产品推荐
相关产品推荐

