[转] 如何让代码可测试化(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; } } |
暂时先写到这里,呵呵...
[转] 如何让代码可测试化(C#)的更多相关文章
- 测试化工具XCTestCase
layout: post title: "Xcode 7智能测试化工具XCTest学习" subtitle: "Xcode 7智能测试化工具XCTest学习" ...
- 来自ebay内部的「软件测试」学习资料,覆盖GUI、API自动化、代码级测试及性能测试等,Python等,拿走不谢!...
在软件测试领域从业蛮久了,常有人会问我: 刚入测试一年,很迷茫,觉得没啥好做的-- 测试在公司真的不受重视,我是不是去转型做开发会更好? 资深的测试架构师的发展路径是怎么样的?我平时该怎么学习? 我 ...
- mvn编写主代码与测试代码
maven编写主代码与测试代码 3.2 编写主代码 项目主代码和测试代码不同,项目的主代码会被打包到最终的构件中(比如jar),而测试代码只在运行测试时用到,不会被打包.默认情况下,Maven假设项目 ...
- js代码如何测试代码运行时间
function add(){ //这里放要执行的代码 } //开始测试并输出 function test() { var start=new Date().getTime(); add(); var ...
- Java代码安全测试解决方案
Java代码安全测试解决方案: http://gdtesting.com/product.php?id=106
- maven编写主代码与测试代码
3.2 编写主代码 项目主代码和测试代码不同,项目的主代码会被打包到最终的构件中(比如jar),而测试代码只在运行测试时用到,不会被打包.默认情况下,Maven假设项目主代码位于src/main/ja ...
- R︱Rstudio 1.0版本尝鲜(R notebook、下载链接、sparkR、代码时间测试profile)
每每以为攀得众山小,可.每每又切实来到起点,大牛们,缓缓脚步来俺笔记葩分享一下吧,please~ --------------------------- 2016年11月1日,RStudio 1.0版 ...
- 使用Jenkins结合Gogs和SonarQube对项目代码进行测试、部署、回滚,以及使用keepalived+haproxy调度至后端tomcat
0 环境说明 主tomcat:192.168.0.112 备tomcat:192.168.0.183 haproxy+keepalived-1:192.168.0.156 haproxy+keepal ...
- Tars | 第7篇 TarsJava Subset最终代码的测试方案设计
目录 前言 1. SubsetConf配置项的结构 1.1 SubsetConf 1.2 RatioConfig 1.3 KeyConfig 1.4 KeyRoute 1.5 SubsetConf的结 ...
随机推荐
- brew安装指定版本boost
brew 如何安装指定版本的boost brew uninstall boost brew install boost@1.57 brew link boost@1.57 --force --over ...
- ClamAV学习【5】—— cli_scanpe函数浏览
这近2000行的代码,要是没有Source Insight,都不知道怎么看下去.跟着跟着来到了PE文件查杀的地方,发现前面都中规中矩地进行PE属性检查,中间一段开始扫描每个区块,然后和特征库的size ...
- MySQL大数据量的导入
最近在公司备份数据库数据,简单的看了一下.当然我用的是简单的手动备份. 第一:其实最好的方法是直接用: mysqldump -u用户名 -p密码 数据库名 < 数据库名.sql 在linux在操 ...
- php留言系统(9)
1.参照之前的(mvc框架总结)将整体框架定下来之后,那么请求默认参数将变为: //默认请求首页: //P=front //C=fIndex //A=show 1.1 找到控制器fIndexC ...
- [Objective-C语言教程]类别(28)
有时,可能会发现希望通过添加仅在某些情况下有用的行为来扩展现有类. 要向现有类添加此类扩展,Objective-C提供了类别和扩展. 如果需要向现有类添加方法,或许为了添加功能以便在应用程序中更容易地 ...
- python基础知识梳理----2格式化输出,替换符
一:格式化输出 1: 格式: 例子: name=input('请输入name') print('名字是%s'%name) %s就是代表字符串串占位符,除此之外,还有%d, 是数字占位符, 如果把上⾯面 ...
- FlowPortal-BPM——移动手机端配置与IIS发布
一.移动手机端配置 (1)VS打开文件夹iAnyWhere,配置config文件 (2)BPM-Web文件config中设置(设置为外网网址) 二.BPM设置 勾选移动审批可以设置要展示的字段信息,修 ...
- 小M的作物 最小割最大流
题目描述 小M在MC里开辟了两块巨大的耕地A和B(你可以认为容量是无穷),现在,小P有n中作物的种子,每种作物的种子有1个(就是可以种一棵作物)(用1...n编号). 现在,第i种作物种植在A中种植可 ...
- python 全栈开发:逻辑运算
基础运算符 逻辑运算: 优先级:()> not > and >or 数字转bool值,0为False,非零的数字为True. 1. print(2 > 1 and 1 < ...
- 利用CSS 修改input=radio的默认样式(改成选择框)
html部分: <input id="item2" type="radio" name="item"> <label fo ...