最近在看.net單元測試藝術,我也喜歡單元測試,這裡寫一下如何在測試中使用模擬對象。 開發的過程中,我們都會遇到對象間的依賴,比如依賴資料庫或文件,這時,我們需要使用模擬對象,來進行測試,我們可以手寫模擬對象,當然也可以使用模擬框架。 假如有這樣的一個需求,當用戶登陸時,我需要對用戶名和密碼進行驗證...
最近在看.net單元測試藝術,我也喜歡單元測試,這裡寫一下如何在測試中使用模擬對象。
開發的過程中,我們都會遇到對象間的依賴,比如依賴資料庫或文件,這時,我們需要使用模擬對象,來進行測試,我們可以手寫模擬對象,當然也可以使用模擬框架。
假如有這樣的一個需求,當用戶登陸時,我需要對用戶名和密碼進行驗證,然後再將用戶名寫入日誌中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public class MyLogin
{
public ILog Log { get ; set ; }
public bool Valid( string userName, string passWord)
{
var isValid = userName == "admin" && passWord == "123456" ;
Log.Write(userName);
return isValid;
}
}
public interface ILog
{
void Write( string message);
}
}
|
上面的代碼在驗證完登陸信息後,需要嚮日志中寫入用戶名,由於寫入日誌可能依賴於文件或資料庫,我們可能很難進行測試,所以,這裡使用模擬對象進行測試。手寫模擬對象,代碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
[TestFixture]
public class MyLoginTest
{
[Test]
public void Vaild_Test()
{
MyLogin login = new MyLogin();
var log = new TestLog();
login.Log = log;
var userNmae = "admin" ;
var passWord = "123456" ;
var isLogin = login.Valid(userNmae, passWord);
Assert.AreEqual(isLogin, true );
Assert.AreEqual(log.Message, userNmae);
}
}
public class TestLog : ILog
{
public string Message;
public void Write( string message)
{
this .Message = message;
}
}
|
這裡我們定義了一個對象TestLog對象,該對象就是一個模擬對像,繼承了ILog介面。該測試中,一共進行了兩項測試。一項是:驗證用戶名和密碼是否輸入正確。另一項是:驗證用戶寫入日誌的信息是否正確(比如應該寫入用戶名,結果把密碼寫入了日誌,測試會無法通過)。
這裡我們區分一下模擬對象與樁對象。上一節中,我們講過樁對象的定義,那麼模擬對象與樁對象是什麼關係呢?
模擬對象與樁對象在寫法上區別很小,關鍵在於模擬對象需要進行斷言,也就是說模擬對象可以導致測試失敗。樁對象只是為了方便測試所定義的一個對象,不需要進行斷言,所以,樁對象永遠不會導致測試失敗。
上面的測試中,如果我們去掉最後一行代碼,即我們不進行寫入日誌的斷言,則該對象就是一個樁對象。
1 |
Assert.AreEqual(log.Message, userNmae);
|
上面的模擬對象是我們自己寫的,自己寫模擬對象比較費時,我們可以使用模擬框架進行編寫。這裡我使用了Rhino Mocks框架。如果要執行下麵的代碼,需要下載Rhino.Mocks.dll文件,然後直接引用即可。
測試框架這裡我選用了NUnit框架。測試代碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
[TestFixture]
public class MyLoginTest
{
[Test]
public void Mock_Vaild_Test()
{
MockRepository mock = new MockRepository();
var log = mock.DynamicMock<ILog>();
var userName = "admin" ;
var passWord = "123456" ;
using (mock.Record())
{
log.Write(userName);
}
MyLogin login = new MyLogin();
login.Log = log;
var isLogin = login.Valid(userName, passWord);
Assert.AreEqual(isLogin, true );
mock.VerifyAll();
}
|
這裡我沒有編寫一個類去繼承ILog介面,而是通過模擬框架,動態生成了一個ILog對象。代碼是這句:
1 2 3 |
MockRepository mock = new MockRepository();
var log = mock.DynamicMock<ILog>();
|
這裡便生成了Log對象。通過錄製-回放的模式進行模擬對象測試,首先需要定義我們的期望行為,最後驗證實際行為與期望行為是否一致。這裡,需要錄製我們期望行為,代碼如下:
1 2 3 4 |
using (mock.Record())
{
log.Write(userName);
}
|
這裡我們期望嚮日志中寫入用戶名。再通過回放來進行驗證,代碼如下:
1 |
mock.VerifyAll();
|
該方法會驗證,期望嚮日志中寫入的信息與實際嚮日志中寫入的信息是否一致,如果不一致,測試失敗。
這裡我們便完成了使用模擬框架進行單元測試。如果我們不需要測試日誌寫入方法,則把模擬對象換成樁對象就可以了,生成樁對象的方法如下:
1 2 3 |
MockRepository mock = new MockRepository();
var log = mock.Stub<ILog>();
|
把回放的方法(mock.VerifyAll())去掉,就完成了模擬對象向樁對象的轉變。註意,這裡錄製的代碼還是需要的。
總結:編寫模擬對象和樁對象是非常有意義的,使用框架可以幫助我們簡化單元測試。一般情況下,一個測試中,可以有多個樁對象,但最好只有一個模擬對象。模擬對象太多,證明一個測試方法做了太多項測試,不利於維護測試代碼,一旦代碼變改,很容易使單元測試失敗。
下一節,寫一下測試框架的一些常用功能,如:如何模擬異常、如何模擬返回值等。。。