如何 Mock 非虛方法和密封類?
更好的排版建議到我的博客中閱讀:
http://www.dozer.cc/2012/11/how-to-mock-non-virtual-method-and-sealed-class/
問題
很常見的問題,沒有接口,那如何 Mock 非虛方法和密封類?
我在上一篇文章(單元測試有感)中介紹了單元測試的原則,也提到了一些技巧,但是代碼是以前寫的,總會有很多不能克服的地方,還有也不可能把所有的方法改成 vitrual ,或者所有的類都有接口。
尋找
一開始搜索:mock non-virtual ,找到了一篇文章:傳送門
文中提到了一個神器:Typemock,貌似它可以實現,但是它是收費的…
大致看了看它的原理,和 PostSharp (PostSharp是用來做 AOP 的)差不多,都是會去修改編譯完成的 dll 文件,簡單粗暴!
雖然粗暴,但貌似的確是一個不錯的方法,別的項目正常編譯,Test 項目中為了測試,把所有的方法加上 Virtual 關鍵字,這樣不就行了嗎?
思路是清晰了,可惜工具都是收費了,直到看到了老趙的博客。
- http://blog.zhaojie.me/2012/01/make-things-mockable-with-mono-cecil.html
- http://www.findex.cn/item.php?id=421685
上面的鏈接是老趙的原文,可惜他好像誤操作,被另一篇文章替換了。
下面的鏈接是別人轉載的,可以看,雖然代碼縮進都不對。
另外,老趙的文章提供了一個很好的思路,但是沒有后續具體的操作細節,我也摸索了很久。所以,下面我會給大家介紹一下完整的、具體的實現步驟。
實現思路和技術細節
實現思路其實收費的 Mock 工具已經提供了:
- 項目中按照之前的設計原則,編寫自己的代碼;
- 測試項目每次編譯完成后,運行一個程序,修改需要 Mock 的 dll ;
- 利用 Moq 等 Mock 框架,在運行時動態生成代理類;
這里,只會修改復制到 Test 運行目錄的 dll,所以不會影響別的項目。
技術細節的話,這里就需要用到老趙博客中提到的 Mono.Cecil 了,建議用 NuGet 獲取最新版本。
Mono.Cecil 可以幫助你修改編譯好的 dll 文件。
核心代碼如下(這部分邏輯由老趙提供,我做了一定的修改):
private static void OverWrite(string file, bool hasSymbols)
{
var asmDef = AssemblyDefinition.ReadAssembly(file,
new ReaderParameters { ReadSymbols = hasSymbols });
var classTypes = asmDef.Modules
.SelectMany(m => m.Types)
.Where(t => t.IsClass)
.ToList();
foreach (var type in classTypes)
{
if (type.IsSealed)
{
type.IsSealed = false;
}
foreach (var method in type.Methods)
{
if (method.IsStatic) continue;
if (method.IsConstructor) continue;
if (method.IsAbstract) continue;
if (!method.IsVirtual)
{
method.IsVirtual = true;
method.IsNewSlot = true;
method.IsReuseSlot = false;
}
else
{
method.IsFinal = false;
}
}
}
asmDef.Write(file, new WriterParameters { WriteSymbols = hasSymbols });
}
只要把這個代碼封裝成一個控制臺應用程序,每次編譯測試項目后運行一下即可。
具體實現步驟
源代碼解決方案結構
MockHelper 是核心工具,作用就是修改編譯好的 dll,一般情況下也只要使用這個即可,別的幾個項目只是用來演示的。
TestDll 內包含了一個密封類和非虛函數,后面會用這個做演示,把它變成可以 Mock 的。
Test 項目就是一個 MSTest 項目,里面演示了怎么使用 MockHelper。
NUnit 項目同樣是一個演示的測試項目,但是用的是 NUnit。
MockHelper 的使用
這個控制臺應用程序其實沒有什么難度,核心代碼上面已經貼出來了。
另外使用的時候需要復制 MockHelper.exe、mock.txt 和 Mono.Cecil*.dll 到你的測試項目中,一共六個文件。
使用方法就是直接運行這個控制臺應用程序,然后可以傳入一個參數:代表 dll 所在的文件夾。如果不傳參數的話默認是在運行目錄。
然后把你需要修改的 dll 全部寫到 mock.txt 中。
配置自動運行 MockHelper
把 MockHelper 復制過去后的關鍵就是要讓這個 exe 可以自動運行啦!
這里用的是:后期生成事件命令行
右擊項目 — 屬性 — 生成事件 — 后期生成事件命令行:
"$(ProjectDir)MockHelper\MockHelper.exe"
這里不用傳參數,因為運行這個工具的是 Test 項目,而這個項目默認的運行位置就是 bin/Debug|Release,所以需要修改的 dll 就在下面。
編寫測試代碼
TestDll 是非虛函數,而且是密封類:
public sealed class TestClass : TestClassBase
{
public string NormalMethod()
{
return "TestClass";
}
public override string VirtualMethod()
{
return base.VirtualMethod();
}
public sealed override string SealedMethod()
{
return base.VirtualMethod();
}
public override string AbstractMethod()
{
return "TestClass";
}
}
public abstract class TestClassBase
{
public virtual string VirtualMethod()
{
return "TestClass";
}
public virtual string SealedMethod()
{
return "TestClass";
}
public abstract string AbstractMethod();
}
測試代碼如下:
[TestClass]
public class UnitTest
{
[TestMethod]
public void TestMethod1()
{
var test = new Mock<TestClass>();
test.Setup(t => t.NormalMethod()).Returns("Mock");
test.Setup(t => t.VirtualMethod()).Returns("Mock");
test.Setup(t => t.SealedMethod()).Returns("Mock");
test.Setup(t => t.AbstractMethod()).Returns("Mock");
Assert.AreEqual(test.Object.NormalMethod(), "Mock");
Assert.AreEqual(test.Object.VirtualMethod(), "Mock");
Assert.AreEqual(test.Object.SealedMethod(), "Mock");
Assert.AreEqual(test.Object.AbstractMethod(), "Mock");
}
}
MSTest 運行結果如下:
NUnit 運行結果如下:
去掉這個工具后會報如下錯誤:
注意事項
不要看上面的步驟簡單,我在配置這個的時候走了很多彎路,這里也跟大家分享一下:
一定要用 Moq 等 Mock 框架
為什么一定要自動 Mock 框架?它的核心不就是繼承一個類嗎?
因為這個工具是在代碼編譯后才去修改 IL 代碼的。也就是說,在編寫的時候,它依然是密封類或者是非虛方法。
所以你如果自己去編寫的話,是無法編譯通過的。
那自動 Mock 框架為何可以呢?
因為這些框架是在運行的時候動態生成一個類去繼承需要 Mock 的類的。在運行的時候,這個類已經被修改過了,所以是不會出錯的。
注意配置一下 MSTest
我在一開始研究這個的時候,遇到了一個很糾結的問題。
在我的 Demo 中它是可以的,但是到了真正的項目中,它卻一直出錯。
后來研究后發現,在出錯的項目中, Test 的運行目錄不是在 bin/Debug 下,
而是在 TestResults/dozer_DOZER-PC 2012-11-27 11_11_22/Out
而且這個文件夾會在每次運行測試的時候創建一個新的。里面的 dll 并不是從 bin/Debug 中復制過去的,所以我工具修改后的 dll 沒有起到作用。
可是為什么我的 Demo 中沒有這樣?后來發現后面一個項目啟用了測試部署功能,雖然不知道這個功能具體的用處,但是取消后出錯的項目也正常了!
取消方法:測試 — 編輯測試設置 — 本地(另一個也要同樣配置) — 部署 — 取消啟用部署。
注意!配置有兩份,要同時取消后才可以生效。
所有測試框架都支持嗎?
原則上,只要你有辦法在運行測試之前跑一下這個工具就可以支持所有的測試框架。
從上面可以看到,MSTest 和 NUnit 的配置方法是完全一樣的。
經過測試,我們公司的自動化部署、測試框架是可以支持這個的,別的環境可能需要一些修改和配置,難度并不是很大。
最后
項目地址:https://github.com/dozer47528/MockHelper
最后,感謝老趙提供的思路!我這里其實只是具體實現一下。
其實,這個是無奈之舉,大家最好還是老老實實地多用接口吧!






浙公網安備 33010602011771號