让代码可测试化

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

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#)的更多相关文章

  1. 测试化工具XCTestCase

    layout: post title: "Xcode 7智能测试化工具XCTest学习" subtitle: "Xcode 7智能测试化工具XCTest学习" ...

  2. 来自ebay内部的「软件测试」学习资料,覆盖GUI、API自动化、代码级测试及性能测试等,Python等,拿走不谢!...

    在软件测试领域从业蛮久了,常有人会问我: 刚入测试一年,很迷茫,觉得没啥好做的-- 测试在公司真的不受重视,我是不是去转型做开发会更好?  资深的测试架构师的发展路径是怎么样的?我平时该怎么学习? 我 ...

  3. mvn编写主代码与测试代码

    maven编写主代码与测试代码 3.2 编写主代码 项目主代码和测试代码不同,项目的主代码会被打包到最终的构件中(比如jar),而测试代码只在运行测试时用到,不会被打包.默认情况下,Maven假设项目 ...

  4. js代码如何测试代码运行时间

    function add(){ //这里放要执行的代码 } //开始测试并输出 function test() { var start=new Date().getTime(); add(); var ...

  5. Java代码安全测试解决方案

    Java代码安全测试解决方案: http://gdtesting.com/product.php?id=106

  6. maven编写主代码与测试代码

    3.2 编写主代码 项目主代码和测试代码不同,项目的主代码会被打包到最终的构件中(比如jar),而测试代码只在运行测试时用到,不会被打包.默认情况下,Maven假设项目主代码位于src/main/ja ...

  7. R︱Rstudio 1.0版本尝鲜(R notebook、下载链接、sparkR、代码时间测试profile)

    每每以为攀得众山小,可.每每又切实来到起点,大牛们,缓缓脚步来俺笔记葩分享一下吧,please~ --------------------------- 2016年11月1日,RStudio 1.0版 ...

  8. 使用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 ...

  9. Tars | 第7篇 TarsJava Subset最终代码的测试方案设计

    目录 前言 1. SubsetConf配置项的结构 1.1 SubsetConf 1.2 RatioConfig 1.3 KeyConfig 1.4 KeyRoute 1.5 SubsetConf的结 ...

随机推荐

  1. 【cocos2d-x 手游研发小技巧(3)Android界面分辨率适配方案】

    先感叹一下吧~~android的各种分辨率各种适配虐我千百遍,每次新项目我依旧待它如初恋···· 每家公司都有自己项目工程适配的方案,这种东西就是没有最好,只有最适合!!! 这次新项目专项针对andr ...

  2. c语言第六次作业---结构体&文件

    1.本章学习总结 1.1思维导图 1.2学习体会 这次应该是本学期最后一次博客了,总结一下这个学期的学习,一开始就基础薄弱还一直畏难一直懒惰,不想去解决问题导致后面问题越来越多就觉得学习越来越难,后面 ...

  3. 73. 矩阵置零 leetcode JAVA

    题目: 给定一个 m x n 的矩阵,如果一个元素为 0,则将其所在行和列的所有元素都设为 0.请使用原地算法. 示例 1: 输入: [   [1,1,1],   [1,0,1],   [1,1,1] ...

  4. css3半圆

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  5. isset()、empty()、is_NULL()的区别

    1,isset():变量不存在,或变量为null,返回false,否则返回true: 2,empty():变量不存在,或变量为null,返回true,另外"".0."0& ...

  6. 十招谷歌 Google 搜索

    十招谷歌搜索 一.或者 OR 二.网址 insite:example.com keyword 三.大约 1.类似查询(记得) ~keyword 2.模糊查询(记得) key*****word 3.模糊 ...

  7. linux中创建一个回收站

      1. mkdir /tmp/trash_tmp 建立一个回收站目录 2. vi /bin/trash 编辑一个文件     mv $@ /tmp/trash_tmp     :wq 保存退出 3. ...

  8. 细化Azure RBAC权限

    Azure RBAC权限的细化一直是比较繁琐的事情,以下示例抛砖引玉,供大家参考 客户需求: 新用户在指定资源组下权限需求如下: 一.禁止以下权限 1. 调整虚拟机大小配置 2. 删除&停止虚 ...

  9. .NET熔断之Polly

    1. Net Core 中有一个被.Net 基金会认可的库 Polly,可以用来简化熔断降级的处理.主要功能:重试(Retry):断路器(Circuit-breaker):超时检测(Timeout): ...

  10. golang在gitlab中的工作流

    在敏捷开发的时代, 快速的编码, code review, 测试, 部署, 是提升程序员效率的关键. 同时在基础工具完备的如今, 我们甚至无需过多的操作就可以轻松实现上述步骤, 本文就以gitlab为 ...