讓代碼可測試化
本篇介紹如何把我們目前最常見的代碼轉換為可以單元測試的代碼,針對業務邏輯層來實現可測試性,我們以銀行轉賬為例,通常代碼如下:
public class TransferController { private TransferDAL dal = new TransferDAL(); public bool TransferMoney(string fromAccount, string toAccount, decimal money) { //驗證:比如賬號是否存在、賬號中是否有足夠的錢用來轉賬 if (fromAccount == null || fromAccount.Trim().Length == 0) return false; if (toAccount == null || toAccount.Trim().Length == 0) return false; if (IsExistAccount(fromAccount))//檢查from賬號是否存在 return false; if (IsAccountHasEnoughMoney(fromAccount))//檢查from賬號中是否有足夠的錢用來轉賬 return false;
//更新數據庫 dal.TransferMoney(fromAccount, toAccount, money);
//發送郵件 EmailSender.SendEmail("aaa@aa.com", "xxxxxxxx", "yyyyyyyyyy");
return true; }
private bool IsAccountHasEnoughMoney(string fromAccount) { throw new System.NotImplementedException(); }
private bool IsExistAccount(string fromAccount) { throw new System.NotImplementedException(); } }
|
相應sql語句如下: public void TransferMoney(string fromAccount, string toAccount, decimal money) { string sql = @"
UPDATE Accounts SET Money=Money-@Money WHERE Account=@FromAccount UPDATE Accounts SET Money=Money+@Money WHERE Account=@FromAccount
"; }
|
扎眼一看,這轉賬操作的邏輯寫在了sql語句中(沒有弱化外部操作),這樣就會導致對業務邏輯代碼的不可測試性,因此需要重構轉賬的計算部分,改成如下:
public class TransferController { private TransferDAL dal = new TransferDAL(); public bool TransferMoney(string fromAccount, string toAccount, decimal money) { //驗證:比如賬號是否存在、賬號中是否有足夠的錢用來轉賬 if (fromAccount == null || fromAccount.Trim().Length == 0) return false; if (toAccount == null || toAccount.Trim().Length == 0) return false; if (IsExistAccount(fromAccount))//檢查from賬號是否存在 return false; if (IsAccountHasEnoughMoney(fromAccount))//檢查from賬號中是否有足夠的錢用來轉賬 return false;
//更新數據庫 using(TransactionScope ts=new TransactionScope()) { dal.MinuseMoney(fromAccount, money); dal.PlusMoney(toAccount, money); ts.Complete(); }
//發送郵件 EmailSender.SendEmail("aaa@aa.com", "xxxxxxxx", "yyyyyyyyyy");
return true; }
private bool IsAccountHasEnoughMoney(string fromAccount) { throw new System.NotImplementedException(); }
private bool IsExistAccount(string fromAccount) { throw new System.NotImplementedException(); } } |
相對於業務邏輯層來說,分析出外部接口有:郵件發送、數據訪問對象,因此增加這2個接口到代碼中,變成如下:
public class TransferController { private ITransferDAO dao = new TransferDAL(); private IEmailSender emailSender=new XXXXXXXXXXXXXXX();//由於一般的email發送類都是static的,不能new,這部分先留着,等下一步解決
public bool TransferMoney(string fromAccount, string toAccount, decimal money) { //驗證:比如賬號是否存在、賬號中是否有足夠的錢用來轉賬 if (fromAccount == null || fromAccount.Trim().Length == 0) return false; if (toAccount == null || toAccount.Trim().Length == 0) return false; if (IsExistAccount(fromAccount))//檢查from賬號是否存在 return false; if (IsAccountHasEnoughMoney(fromAccount))//檢查from賬號中是否有足夠的錢用來轉賬 return false;
//更新數據庫 using(TransactionScope ts=new TransactionScope()) { this.dao.MinuseMoney(fromAccount, money); this.dao.PlusMoney(toAccount, money); ts.Complete(); }
//發送郵件 this.emailSender.SendEmail("aaa@aa.com", "xxxxxxxx", "yyyyyyyyyy");
return true; }
private bool IsAccountHasEnoughMoney(string fromAccount) { throw new System.NotImplementedException(); }
private bool IsExistAccount(string fromAccount) { throw new System.NotImplementedException(); } } |
但是此時的2個接口,實際系統運行過程中還是會強耦合2個具體類,還是不可測試,怎么辦呢?利用構造函數注入:
public class TransferController { private ITransferDAO dao; private IEmailSender emailSender;
public TransferController()//實際運行時可以用這個構造 { dao = new TransferDAL(); emailSender = new EmailSenderAgent(); }
public TransferController(ITransferDAO dao, IEmailSender emailSender)//測試時用這個構造注入Fake對象 { this.dao = dao; this.emailSender = emailSender; }
public bool TransferMoney(string fromAccount, string toAccount, decimal money) { //驗證:比如賬號是否存在、賬號中是否有足夠的錢用來轉賬 if (fromAccount == null || fromAccount.Trim().Length == 0) return false; if (toAccount == null || toAccount.Trim().Length == 0) return false; if (IsExistAccount(fromAccount))//檢查from賬號是否存在 return false; if (IsAccountHasEnoughMoney(fromAccount))//檢查from賬號中是否有足夠的錢用來轉賬 return false;
//更新數據庫 using(TransactionScope ts=new TransactionScope()) { this.dao.MinuseMoney(fromAccount, money); this.dao.PlusMoney(toAccount, money); ts.Complete(); }
//發送郵件 this.emailSender.SendEmail("aaa@aa.com", "xxxxxxxx", "yyyyyyyyyy");
return true; }
private bool IsAccountHasEnoughMoney(string fromAccount) { throw new System.NotImplementedException(); }
private bool IsExistAccount(string fromAccount) { throw new System.NotImplementedException(); } } |
終於,可以編寫單元測試了,看下面:
class TransferMoneyTest { public void TransferMoney_Validate_FromAccount_Null_Test() { string fromAccount=null; string toAccount="bbbbbbbbbbbb"; decimal money=100;
TransferController ctl = new TransferController(null, null);//因為這個測試用不到這2個接口,所以用了null bool real= ctl.TransferMoney(fromAccount, toAccount, money);
Assert.IsFalse(real); } public void TransferMoney_Validate_FromAccount_Empty_Test() { string fromAccount = ""; string toAccount = "bbbbbbbbbbbb"; decimal money = 100;
TransferController ctl = new TransferController(null, null);//因為這個測試用不到這2個接口,所以用了null bool real = ctl.TransferMoney(fromAccount, toAccount, money);
Assert.IsFalse(real); }
public void TransferMoney_Validate_FromAccount_AllSpace_Test() { string fromAccount = " "; string toAccount = "bbbbbbbbbbbb"; decimal money = 100;
TransferController ctl = new TransferController(null, null);//因為這個測試用不到這2個接口,所以用了null bool real = ctl.TransferMoney(fromAccount, toAccount, money);
Assert.IsFalse(real); }
public void TransferMoney_Validate_FromAccount_NotExist_Test() { string fromAccount = "11111111111111"; string toAccount = "bbbbbbbbbbbb"; decimal money = 100;
ITransferDAO dao = new FakeTransferDAO_NullAccount();
TransferController ctl = new TransferController(dao, null);//因為這個測試用不到IEmailSender接口,所以用了null bool real = ctl.TransferMoney(fromAccount, toAccount, money);
Assert.IsFalse(real); }
public void TransferMoney_Validate_FromAccount_NotEnoughMoney_Test() { string fromAccount = "11111111111111"; string toAccount = "bbbbbbbbbbbb"; decimal money = 100;
ITransferDAO dao = new FakeTransferDAO_NotEnoughMoney();
TransferController ctl = new TransferController(dao, null);//因為這個測試用不到IEmailSender接口,所以用了null bool real = ctl.TransferMoney(fromAccount, toAccount, money);
Assert.IsFalse(real); } } |
用到了如下2個Fake類 class FakeTransferDAO_NullAccount : ITransferDAO { public void MinuseMoney(string fromAccount, decimal money) { throw new NotImplementedException(); }
public void PlusMoney(string toAccount, decimal money) { throw new NotImplementedException(); }
public Account GetAccount(string accountId) { return null; } }
class FakeTransferDAO_NotEnoughMoney: ITransferDAO { public void MinuseMoney(string fromAccount, decimal money) { throw new NotImplementedException(); }
public void PlusMoney(string toAccount, decimal money) { throw new NotImplementedException(); }
public Account GetAccount(string accountId) { Account account = new Account(); account.Money = 20; return account; } } |
暫時先寫到這里,呵呵...