业务简述

  • 关键字段:邀请码最大使用次数UseMaxNumber和允许取消次数CancelUseMaxNumber,已使用次数UsedCount,已取消次数CancelUsedCount。

  • 提交使用邀请码的订单,占用邀请码使用次数。
    在允许取消次数内取消订单,退回邀请码使用次数。
    超过允许取消次数取消订单,不退回邀请码使用次数。

  • 注意点:临界值。

原核心代码(X.1版)

“` csharp
public ResponseMessage 示例方法_ProcessCode(X used,YY invitecodedto)
{
var isoverinvite = false;//已经超过取消次数
var iswilloverinvite = false;//将要超出取消次数
long inviteNum = 0;//本次邀约使用次数
//判断是否已经超过取消次数,或者将要超出取消次数。
if (invitecodedto != null && invitecodedto.IsLimitCancelUse)
{
if (invitecodedto.CancelUsedCount > invitecodedto.CancelUseMaxNumber)
{
isoverinvite = true;
}
else if (invitecodedto.CancelUsedCount + used.InviteNum > invitecodedto.CancelUseMaxNumber)
{
iswilloverinvite = true;
}
}

ResponseMessage<long> inviteuseres = null;
//邀约码不为null,递增取消次数,扣减使用次数。
if (invitecodedto != null)
{//递增已取消次数var cancelcount = _codeService.IncCancelUseCount(invitecodedto.Id, (int)used.InviteNum);if (isoverinvite){}else if (iswilloverinvite){inviteNum = invitecodedto.CancelUseMaxNumber > cancelcount.Body ? invitecodedto.CancelUseMaxNumber - cancelcount.Body : cancelcount.Body - invitecodedto.CancelUseMaxNumber;//将要超出的,只退出部分。inviteuseres = _codeService.IncUseCount(invitecodedto.Id, -(int)(inviteNum));}else{inviteNum = used.InviteNum;//未超出取消次数的,全数退回。inviteuseres = _codeService.IncUseCount(invitecodedto.Id, -(int)inviteNum);}
}
.
.
.
//更新取消日志。
//更新码相关的各种状态。

}
“`
### X.1版代码引起问题
* 使用次数为1,允许取消次数为1时,运行正确。

  • 使用次数为1,允许取消次数为2时,结果错误。
    csharp
    >>测试流程目标:【每次报名都为1人】报名一次,取消一次,再报名一次,再取消一次后。再报名一次后,后续不能再报名。
    >>实际效果:仍然还能报名一次。
    >>原因分析:订单第二次取消后。已取消次数为2,允许取消次数为2,这个判断无法命中。
    if (invitecodedto.CancelUsedCount > invitecodedto.CancelUseMaxNumber)
    {
    isoverinvite = true;
    }

优化后代码(X.2版)

csharp
var isoverinvite = false;//已经超过取消次数
var iswilloverinvite = false;//将要超出取消次数
long inviteNum = 0;//本次邀约使用次数
if (invitecodedto != null && invitecodedto.IsLimitCancelUse)
{
//这里多加了个=号
if (invitecodedto.CancelUsedCount >= invitecodedto.CancelUseMaxNumber)
{
isoverinvite = true;
}//这里也多加了个=号
else if (invitecodedto.CancelUsedCount + used.InviteNum >= invitecodedto.CancelUseMaxNumber)
{
iswilloverinvite = true;
}
}

X.2版代码引起问题

  • X.2版修复了上个问题。但仍有场景覆盖不够。

  • 使用次数为2,允许取消次数为2时,结果错误。

    >>测试流程目标:报名一次(1人),取消,再报名一次(2人),再取消。预期仍可以继续报名1人。>>实际效果:无法继续报名。>>原因分析,第二次取消请求时:>>>根据判断 已取消次数加上邀约人数大于允许取消次数,1+2>2,所以是将要超出允许取消次数。..else if (invitecodedto.CancelUsedCount + used.InviteNum > invitecodedto.CancelUseMaxNumber){iswilloverinvite = true;}..>>>再来看下扣减使用次数的部分。CancelUseMaxNumber为2,cancelcount.Body为2,>>>所以结果是:2>2?(2-2):(2-2),返回0,意思是没有返回使用次数。..else if (iswilloverinvite){inviteNum = invitecodedto.CancelUseMaxNumber > cancelcount.Body ? invitecodedto.CancelUseMaxNumber - cancelcount.Body : cancelcount.Body - invitecodedto.CancelUseMaxNumber;//将要超出的,只退出部分。inviteuseres = _codeService.IncUseCount(invitecodedto.Id, -(int)(inviteNum));}..>>>正确结果应该是:因为已经取消过一次了,这次报名2人,如按正常应该是总取消3次,但允许取消次数是2次,所以使用次数只能返回一次。>>>预期结果和实际结果不符。

思考

  • > 上面问题是由于退回使用次数计算不对引起的。
  • > 改动后验证流程是很繁琐的,要配置邀请码,要填写报名信息,要重复提交,重复取消订单好几次来验证逻辑。
  • > 组合条件是千变万化的。
  • > 这个业务重点是测试取消订单后对于使用次数和允许取消次数的正确性。如全流程走一下,是浪费时间的。
  • > 所以为保证正确性及方便,这个必须支持单元测试。单元测试才能快速试错。

影响单元测试的几点

  • > 业务耦合。这个取消邀请方法内有处理邀请码使用次数和取消次数的,也有处理取消记录,维护各个状态等。不符合单一功能原则。
  • > 数据库依赖,影响mock数据及执行后的结果对比。
  • > 重复执行后结果的积累。如订单取消后,邀请码的使用次数和允许取消次数都会变,作为下次单元测试的依据。

改进建议

  • > 对打算单元测试的代码,要保持功能单一,不耦合其他业务。
  • > 面向接口编程,依赖注入。与具体的实现解耦,方便单元测试。
  • > 方法体尽量移除仓储部分逻辑或者mock一个仓储对象替代。
  • > 必须方便批量单元测试。

单元测试前置–Nuget包依赖

  • >Xunit:一个开发测试框架,它支持测试驱动开发,具有极其简单和与框架特征对齐的设计目标。
  • >xunit.runner.visualstudio: 支持Vs调试,运行测试
  • >NSubstitute :一个友好的.net单元测试隔离框架。
  • >Autofac: Ioc容器
//单元测试部分
public class GetTicketDiscounts_Test{       private IXTaDiscountService discountService = null;private IXTaCodeService codeSub = null;public GetTicketDiscounts_Test(){discountService = XTaContainer.Resolve<IXTaDiscountService>();codeSub = NSubstitute.Substitute.For<IXTaCodeService>();}}
//注册部分public static class XTaContainer{public readonly static IContainer _container;static XTaContainer(){// Create your builder.var builder = new ContainerBuilder();//自动注册。var baseType = typeof(IApplication);var assemblys = AppDomain.CurrentDomain.GetAssemblies().ToList();builder.RegisterAssemblyTypes(assemblys.ToArray()).Where(t => baseType.IsAssignableFrom(t) && t != baseType).AsImplementedInterfaces().InstancePerLifetimeScope();//Redisbuilder.Register(n => Substitute.For<ICache>()).As<ICache>().SingleInstance();          //mongodbbuilder.Register(n => Substitute.For<IMongoDbProvider>()).As<IMongoDbProvider>().SingleInstance();_container = builder.Build();}public static T Resolve<T>(){return _container.Resolve<T>();}}

支持单元测试的代码(X.3版-只粘贴相关代码)

//接口
public interface IXTaService : IApplication{ResponseMessage<long> GetReturnUseNum(long invitediscountNum, XTaCodeDto codedto);
}
//实现public class XTaDiscountService : IXTaDiscountService{private readonly IXTaCodeService _codeService;public XTaDiscountService(IXTaCodeService codeService){_codeService = codeService;}//将操作使用次数和取消次数的仓储部分挪出去,这里只计算需要退回的使用次数。public ResponseMessage<long> GetReturnUseNum(long invitediscountNum, XTaCodeDto codedto){//默认是全部退回使用次数。long returnNum = invitediscountNum;if (codedto == null){return ResponseMessage<long>.MakeSucc(0);}//不限制取消的的时候,退回全部使用次数。if (!codedto.IsLimitCancelUse){return ResponseMessage<long>.MakeSucc(returnNum);}//已超过的不处理。if (codedto.CancelUsedCount >= codedto.CancelUseMaxNumber){return ResponseMessage<long>.MakeSucc(0);}//将要超过的。if (codedto.CancelUsedCount + invitediscountNum >= codedto.CancelUseMaxNumber){returnNum = codedto.CancelUsedCount + invitediscountNum - codedto.CancelUseMaxNumber;return ResponseMessage<long>.MakeSucc(returnNum);}return ResponseMessage<long>.MakeSucc(returnNum);}}
>初始化数据private void 验证取消优惠_初始化数据(ref XTaCodeDto codeDto, int usemax = 0, int cancelmax = 0){if (codeDto == null){codeDto = new XTaCodeDto(){Id = "11111",CancelUsedCount = 0,UsedCount = 0,PrivateSetting = new PrivateSetting(){IsLimitCancelUse = true,IsCustomCancelUse = true,CancelUseMaxNumber = 1,IsLimitUse = true,IsCustomUse = true,UseMaxNumber = 1}};}if (cancelmax > 0){codeDto.PrivateSetting.CancelUseMaxNumber = cancelmax;codeDto.CancelUsedCount = 0;}if (usemax > 0){codeDto.PrivateSetting.UseMaxNumber = usemax;codeDto.UsedCount = 0;}}
> 模拟报名使用邀请码,递增使用次数,方便批量测试。private void 初始化数据_模拟报名使用邀请码_递增使用次数(int useNum, XTaCodeDto codeDto){//mock模拟使用邀请码时,递增的邀请码使用次数返回使用次数。var usercount = codeSub.IncUseCount(codeDto.Id, Arg.Any<int>()).Returns(x => new ResponseMessage<long>() { Body = (int)codeDto.UsedCount + x.Arg<int>() });codeDto.UsedCount = codeSub.IncUseCount(codeDto.Id, useNum).Body;}
 > 模拟取消订单,退回使用次数private void 验证取消优惠_退回使用次数_V1ForPrivate(long inviteDiscountNum, XTaCodeDto codeDto){//计算退回使用次数。var res = discountService.GetReturnUseNum(inviteDiscountNum, codeDto);codeDto.UsedCount -= res.Body;codeDto.CancelUsedCount += inviteDiscountNum;}
>实际测试部分[Fact]public void 验证取消优惠_退回使用次数_最大使用一次_允许取消一次(){XTaCodeDto codeDto = null;验证取消优惠_初始化数据(ref codeDto, 1, 1);//第一次报名,取消验证取消优惠_模拟报名使用邀请码_递增使用次数(1, codeDto);验证取消优惠_退回使用次数_V1ForPrivate(1, codeDto);//第一次取消会退回使用次数。Assert.True(codeDto.UsedCount == 0 && codeDto.CancelUsedCount == 1);//第二次报名,取消验证取消优惠_模拟报名使用邀请码_递增使用次数(1, codeDto);验证取消优惠_退回使用次数_V1ForPrivate(1, codeDto);//第二次取消后,超出允许取消次数限制,不会退回Assert.True(codeDto.UsedCount == 1 && codeDto.CancelUsedCount == 2);}    [Fact]public void 验证取消优惠_退回使用次数_最大使用2次_允许取消两次(){XTaCodeDto codeDto = null;验证取消优惠_初始化数据(ref codeDto, 2, 2);验证取消优惠_模拟报名使用邀请码_递增使用次数(1, codeDto);验证取消优惠_退回使用次数_V1ForPrivate(1, codeDto);Assert.True(codeDto.UsedCount == 0 && codeDto.CancelUsedCount == 1);验证取消优惠_模拟报名使用邀请码_递增使用次数(2, codeDto);验证取消优惠_退回使用次数_V1ForPrivate(2, codeDto);Assert.True(codeDto.UsedCount == 1 && codeDto.CancelUsedCount == 3);验证取消优惠_模拟报名使用邀请码_递增使用次数(1, codeDto);验证取消优惠_退回使用次数_V1ForPrivate(1, codeDto);Assert.True(codeDto.UsedCount == 2 && codeDto.CancelUsedCount == 4);}

使用单元测试的好处

  • > 快速验证结果,不用依赖各种数据库/缓存等环境。
  • > 代码指责更单一。
  • > 减少bug
  • > 方便后期持续集成

可参考连接

使用 dotnet test 和 xUnit 在 .NET Core 中进行 C# 单元测试
nsubstitute 介绍
Autofac介绍
单元测试的艺术

.Net单元测试业务实践相关推荐

  1. 领域驱动设计之单元测试最佳实践(二)

    领域驱动设计之单元测试最佳实践(一) 介绍完了DDD案例,我们终于可以进入主题了,本方案的测试代码基于Xunit编写,断言组件采用了FluentAssertions,类似的组件还有Shouldly.另 ...

  2. 敏捷整洁之道 -- 第三章 业务实践

    敏捷整洁之道 -- 第三章 业务实践 0. 引子 1. 计划游戏 1.1 三元分析 1.2 故事和点数 1.3 故事 1.4 故事估算 1.5 对迭代进行管理 1.6 速率 2. 小步发布 3. 验收 ...

  3. 机器学习业务实践之路-李博-专题视频课程

    机器学习业务实践之路-7159人已学习 课程介绍         本系列课程为您讲述如何使用机器学习算法解决业务问题,会以实际业务作为出发点,所有实验都提供实验流程以及实验数据,帮您用短的时间学习机器 ...

  4. 神策数据胡士文:数据智能驱动业务实践

    本文根据神策数据算法工程师胡士文,以<数据智能驱动业务实践>为主题的演讲整理而成,将为您重点介绍以下内容: 依据场景解读如何应用数据智能? Case 1:如何通过机器学习,促进付费用户识别 ...

  5. 一文搞懂 SQL:基础知识和业务实践总结

    作者:cooperyjli,腾讯 CDG 数据分析师 SQL的全称是Structured Query Language(结构化查询语言),是一种古老而简洁的程序设计语言.看似平平无奇,一直被各种吐槽, ...

  6. python 支付宝个人账单_金融支付财务融合业务-实践分享1:订单、账单、交易流水、账套知识解构、原理解析...

    本文作者从实际工作实践出发,结合案例等分享了电商金融支付财务融合中的基本概念和相关原理解析,包括:订单.账单.交易流水和账知识解构,供大家一同参考和学习. 从事电商.进销存.金融.支付.财务的产品同学 ...

  7. 视频PPT互动问答丨数据驱动的业务实践专题

    2021年11月26日,以"数据驱动的业务实践"为主题的『2021数据技术嘉年华·ON LINE』第四期活动已经圆满结束啦!现在给大家安排上干货满满的PPT资源放送及视频回看,没来 ...

  8. @sql 单元测试_SQL单元测试最佳实践

    @sql 单元测试 SQL unit testing is a testing method which allows us to test the smallest, atomic programm ...

  9. 中科大脑知识图谱平台建设及业务实践

    本文首发于 Nebula Graph Community 公众号 "为了支持城市复杂场景下各类需求,中科大脑知识图谱团队设计开发了一套包含本体可视化设计.数据映射.数据抽取.数据写入.图数据 ...

最新文章

  1. C++标准库math
  2. linux查找并删除文件
  3. struts的国际化
  4. 重启大法好!线上常见问题排查手册
  5. MATLAB混合编程视频教程下载 SIMULINK系统仿真视频
  6. 如何用rake tasks 生成migration对应的sql
  7. testbench文件显示波形_modelsim仿真没有波形或看不到波形的原因及解决方法 - 全文...
  8. Java集合类源码详解
  9. chrome浏览器最小字体大小限制的解决方案
  10. 《开店有讲究》读书笔记
  11. 多路耦合器(有源分离器)在无线通讯中的应用
  12. windows redis 假死处理
  13. 集合 - Java篇
  14. Landsat8遥感影像批量分割(Arcmap实现)
  15. 修改so文件的关键方法
  16. 计算机excel感叹号,感叹号!在Excel中的用途,你绝对不知道!
  17. 超详细的Redisson实现分布式锁原理解析
  18. 前程无忧推进私有化:CEO甄荣辉持股19%,多次陷入信息泄露风波
  19. 2018ccpc吉林 C:JUSTICE
  20. 遭遇修改系统时间、使用映像劫持的xibgptd.exe,netdde32.exe等2

热门文章

  1. 宁波阿里云代理商:阿里巴巴CEO张勇:阿里云是一家云计算产品公司
  2. 亚马逊云科技 BuildOn - 基于AmazonServerless构建零售创新应用感受心得
  3. html网页制作看板娘原理,博客网页上添加live2D看板娘
  4. 虚拟机安装postfix服务器,CentOS 7 下搭建邮件服务器(bind+postfix+dovecot)
  5. POJ 2560 雀斑
  6. windows下打开 .pages文件?
  7. oracle和勇士,NBA/KD走了没差 7成勇士季票持有者选择续订
  8. OPPOr9手机设置
  9. 20190710项目日志2
  10. kubernetes中给pod加hosts解析