如何通过反射设定私有方法返回值以测试只读遗留代码?
如何Mock只读遗留代码中私有方法的返回值?
问题背景
需要为只读状态的遗留代码编写单元测试,其中OldUntouchableCode类的公共方法DoStuff依赖私有方法DoInternalStuff。该私有方法直接实例化HttpClient调用外部接口,无法通过常规依赖注入Mock,导致DoStuff的返回值不可预测,无法可靠执行单元测试。已通过反射获取到DoInternalStuff的MethodInfo,希望让运行时调用该私有方法时返回指定字符串,忽略其内部逻辑。
示例代码
遗留代码(不可修改)
//This class can't be changed or refactored. It is what it is. public class OldUntouchableCode { public string DoStuff() { var result = DoInternalStuff(); return result + " new stuff"; } private string DoInternalStuff() { //Making external calls that DON'T use the dependency-injected clients. //This means that the external calls can't be mocked. //The output of this call changes depending on what the external call returns. //This makes the return value of this method unpredictable. var client = new HttpClient(); /* * imagine a bunch of code here that uses the HttpClient to make external calls */ return "The result of the external calls"; } }
现有测试代码(不可靠)
var sut = new OldUntouchableCode(); //I need to set the return value of DoInternalStuff() to a specific value, so I can test the DoStuff() method. //This test might or might not pass, depending on what the external call returns. This return value is unpredictable. var actual = sut.DoStuff(); var expected = "expected value new stuff"; Console.WriteLine($" Test {(expected == actual ? "Passed":"Failed")}");
反射获取私有方法代码
var methodInfo = sut.GetType().GetMethod("DoInternalStuff", BindingFlags.NonPublic | BindingFlags.Instance);
解决方案:可行,通过运行时方法拦截实现
直接反射调用私有方法无法替换原方法的执行逻辑,需要借助运行时IL重写或方法拦截框架来修改私有方法的行为,以下是两种成熟的实现方式:
1. 使用微软Fakes框架(适合MS Test环境)
微软Fakes可以在运行时隔离并替换任意方法的实现,包括私有方法,无需修改原代码。
步骤:
- 添加
Microsoft.QualityTools.Testing.FakesNuGet包 - 在项目引用中,右键点击目标程序集(包含
OldUntouchableCode的程序集),选择"添加Fakes程序集"
测试代码示例:
using Microsoft.QualityTools.Testing.Fakes; using Microsoft.VisualStudio.TestTools.UnitTesting; [TestClass] public class OldUntouchableCodeTests { [TestMethod] public void DoStuff_ReturnsExpectedCombinedValue() { using (ShimsContext.Create()) { // 拦截私有方法DoInternalStuff,强制返回指定值 ShimOldUntouchableCode.DoInternalStuff = instance => "expected value"; var sut = new OldUntouchableCode(); var actual = sut.DoStuff(); Assert.AreEqual("expected value new stuff", actual); } } }
2. 使用Harmony库(通用场景,无测试框架限制)
Harmony是一个轻量级的IL重写库,可以灵活替换任意方法(包括私有、静态方法)的实现,兼容性强。
步骤:
- 添加
HarmonyLibNuGet包
测试代码示例:
using HarmonyLib; using Xunit; public class OldUntouchableCodeTests { [Fact] public void DoStuff_ReturnsExpectedCombinedValue() { var harmony = new Harmony("com.yourcompany.test"); var targetMethod = typeof(OldUntouchableCode).GetMethod( "DoInternalStuff", BindingFlags.NonPublic | BindingFlags.Instance ); // 用自定义逻辑替换原方法 harmony.Patch( targetMethod, prefix: new HarmonyMethod(typeof(OldUntouchableCodeTests).GetMethod(nameof(OverrideDoInternalStuff), BindingFlags.NonPublic | BindingFlags.Static)) ); var sut = new OldUntouchableCode(); var actual = sut.DoStuff(); Assert.Equal("expected value new stuff", actual); // 清理补丁,避免影响其他测试 harmony.UnpatchAll(); } private static bool OverrideDoInternalStuff(ref string __result) { // 设置返回值,并返回false跳过原方法的执行 __result = "expected value"; return false; } }
说明
以上两种方式都是在运行时修改方法的执行逻辑,完全不需要改动只读的遗留代码,符合公司政策。选择哪种方案取决于项目使用的测试框架:
- 若使用MS Test,微软Fakes集成更顺畅
- 若用xUnit/NUnit或需要跨平台支持,Harmony是更灵活的选择
内容的提问来源于stack exchange,提问作者Khosro Jadidi
相关产品推荐
相关产品推荐

