[转] 如何让代码可测试化(C#)

时间:2023-03-09 09:46:13
[转] 如何让代码可测试化(C#)

让代码可测试化

本篇介绍如何把我们目前最常见的代码转换为可以单元测试的代码,针对业务逻辑层来实现可测试性,我们以银行转账为例,通常代码如下:

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;

}

}

暂时先写到这里,呵呵...