.Net单元测试业务实践

简介: code[class*="language-"],pre[class*="languag...

业务简述

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

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

  • 注意点:临界值。

原核心代码(X.1版)

public ResponseMessage<bool> 示例方法_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时,结果错误。

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

优化后代码(X.2版)

 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();
           //Redis
            builder.Register(n => Substitute.For<ICache>())
                .As<ICache>().SingleInstance();          
            //mongodb
            builder.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介绍
单元测试的艺术

作者:从此启程/范存威

出处:http://www.cnblogs.com/fancunwei/

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。如文章对您有用,烦请点个推荐再走,感谢! 本博客新开通打赏,鼠标移到右侧打赏浮动处,即可赏博主点零花钱,感谢您的支持!

相关文章
|
23天前
|
安全 Linux 测试技术
提升龙蜥内核测试能力!探究持续性模糊测试优化实践
清华大学软件学院对Anolis OS使用靶向模糊测试方法将测试工作引向修改的代码,进而提高对业务代码的测试能力。
|
1月前
|
SQL 搜索推荐 测试技术
【Havenask实践篇】完整的性能测试
Havenask是阿里巴巴智能引擎事业部自研的开源高性能搜索引擎,深度支持了包括淘宝、天猫、菜鸟、高德、饿了么在内几乎整个阿里的搜索业务。性能测试的目的在于评估搜索引擎在各种负载和条件下的响应速度、稳定性。通过模拟不同的用户行为和查询模式,我们可以揭示潜在的瓶颈、优化索引策略、调整系统配置,并确保Havenask在用户数量激增或数据量剧增时仍能保持稳定运行。本文举例对Havenask进行召回性能测试的一个简单场景,在搭建好Havenask服务并写入数据后,使用wrk对Havenask进行压测,查看QPS和查询耗时等性能指标。
65382 6
|
2月前
|
安全 测试技术
测试团队的一次复盘实践
测试团队的一次复盘实践
144 0
|
3月前
|
安全 jenkins 测试技术
自动化测试与持续集成/持续交付(CI/CD)的实践与应用
自动化测试是现代软件开发不可或缺的环节,它可以有效地提高测试效率、降低测试成本。而持续集成/持续交付(CI/CD)则是一种基于自动化的软件开发流程,能够将代码的开发、构建、测试和部署等过程无缝连接起来,从而实现快速迭代和部署。本文将结合实际案例,介绍自动化测试和CI/CD的实践与应用。
149 2
|
3月前
|
机器学习/深度学习 搜索推荐 算法
推荐系统离线评估方法和评估指标,以及在推荐服务器内部实现A/B测试和解决A/B测试资源紧张的方法。还介绍了如何在TensorFlow中进行模型离线评估实践。
推荐系统离线评估方法和评估指标,以及在推荐服务器内部实现A/B测试和解决A/B测试资源紧张的方法。还介绍了如何在TensorFlow中进行模型离线评估实践。
192 0
|
8天前
|
敏捷开发 监控 前端开发
深入理解自动化测试框架Selenium的架构与实践
【4月更文挑战第16天】 在现代软件开发过程中,自动化测试已成为确保产品质量和加快迭代速度的关键手段。Selenium作为一种广泛使用的自动化测试工具,其开源、跨平台的特性使得它成为业界的首选之一。本文旨在剖析Selenium的核心架构,并结合实际案例探讨其在复杂Web应用测试中的高效实践方法。通过详细解读Selenium组件间的交互机制以及如何优化测试脚本,我们希望为读者提供深入理解Selenium并有效运用于日常测试工作的参考。
14 1
|
9天前
|
自然语言处理 测试技术 API
深入理解自动化测试框架Selenium的设计理念与实践
【4月更文挑战第15天】 在现代软件开发过程中,自动化测试已成为确保产品质量和加速迭代的关键手段。Selenium作为一种广泛使用的自动化测试框架,提供了对多种浏览器和平台的支持,极大地促进了Web应用的功能测试。本文旨在剖析Selenium的核心设计理念,探讨其在实际项目中的应用,并指出常见的误区及最佳实践,以期帮助测试工程师更高效地利用Selenium进行测试工作。
|
17天前
|
安全 测试技术
深入理解白盒测试:方法、工具与实践
【4月更文挑战第7天】 在软件开发的质量控制过程中,白盒测试是确保代码逻辑正确性的关键步骤。不同于黑盒测试关注于功能和系统的外部行为,白盒测试深入到程序内部,检验程序结构和内部逻辑的正确性。本文将探讨白盒测试的核心技术,包括控制流测试、数据流测试以及静态分析等方法,同时介绍当前流行的白盒测试工具,并讨论如何在实际项目中有效实施白盒测试。文章的目标是为软件测试工程师提供一份综合性指南,帮助他们更好地理解和应用白盒测试技术。
|
29天前
|
敏捷开发 测试技术 持续交付
深入探索软件测试自动化:框架与实践
在快速演进的软件行业中,测试自动化已成为确保产品质量和加快上市速度的关键因素。本文将深入分析测试自动化框架的构建要点,探讨其在实际应用中的效益,以及实施过程中可能面临的挑战。通过对比手动测试与自动化测试的优势与局限,本文旨在为读者提供一套系统化的测试自动化实践指南,以支持更高效、可靠的软件开发周期。
11 0
|
30天前
|
传感器 监控 算法
【软件设计师备考 专题 】模块测试的方法和实践
【软件设计师备考 专题 】模块测试的方法和实践
73 0

热门文章

最新文章