让代码可测试化
本篇介绍如何把我们目前最常见的代码转换为可以单元测试的代码,针对业务逻辑层来实现可测试性,我们以银行转账为例,通常代码如下:
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; } } |
暂时先写到这里,呵呵...