.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/

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

相关文章
|
3天前
|
敏捷开发 人工智能 Devops
探索自动化测试的高效策略与实践###
当今软件开发生命周期中,自动化测试已成为提升效率、保障质量的关键工具。本文深入剖析了自动化测试的核心价值,探讨了一系列高效策略,包括选择合适的自动化框架、设计可维护的测试脚本、集成持续集成/持续部署(CI/CD)流程,以及有效管理和维护测试用例库。通过具体案例分析,揭示了这些策略在实际应用中的成效,为软件测试人员提供了宝贵的经验分享和实践指导。 ###
|
3天前
|
机器学习/深度学习 人工智能 jenkins
软件测试中的自动化与持续集成实践
在快速迭代的软件开发过程中,自动化测试和持续集成(CI)是确保代码质量和加速产品上市的关键。本文探讨了自动化测试的重要性、常见的自动化测试工具以及如何将自动化测试整合到持续集成流程中,以提高软件测试的效率和可靠性。通过案例分析,展示了自动化测试和持续集成在实际项目中的应用效果,并提供了实施建议。
|
3天前
|
Java 测试技术 持续交付
探索自动化测试在软件开发中的关键作用与实践
在现代软件开发流程中,自动化测试已成为提升产品质量、加速交付速度的不可或缺的一环。本文深入探讨了自动化测试的重要性,分析了其在不同阶段的应用价值,并结合实际案例阐述了如何有效实施自动化测试策略,以期为读者提供一套可操作的实践指南。
|
27天前
|
测试技术 持续交付 API
深入挖掘探索.NET单元测试
【10月更文挑战第11天】
36 2
|
28天前
|
Java 测试技术 开发者
初学者入门:掌握单元测试的基础与实践
【10月更文挑战第14天】单元测试是一种软件测试方法,它验证软件中的最小可测试单元——通常是单独的函数或类——是否按预期工作。单元测试的目标是确保每个模块在其自身范围内正确无误地运行。这些测试应该独立于其他模块,并且应该能够反复执行而不受外部环境的影响。
50 2
|
4天前
|
Web App开发 敏捷开发 测试技术
探索自动化测试的奥秘:从理论到实践
【10月更文挑战第39天】在软件质量保障的战场上,自动化测试是提升效率和准确性的利器。本文将深入浅出地介绍自动化测试的基本概念、必要性以及如何实施自动化测试。我们将通过一个实际案例,展示如何利用流行的自动化测试工具Selenium进行网页测试,并分享一些实用的技巧和最佳实践。无论你是新手还是有经验的测试工程师,这篇文章都将为你提供宝贵的知识,帮助你在自动化测试的道路上更进一步。
|
4天前
|
敏捷开发 Java 测试技术
探索自动化测试:从理论到实践
【10月更文挑战第39天】在软件开发的海洋中,自动化测试是一艘能够带领团队高效航行的船只。本文将作为你的航海图,指引你理解自动化测试的核心概念,并分享一段实际的代码旅程,让你领略自动化测试的魅力和力量。准备好了吗?让我们启航!
|
9天前
|
测试技术 API Android开发
探索软件测试中的自动化框架选择与实践####
本文深入探讨了软件测试领域内,面对众多自动化测试框架时,如何依据项目特性和团队需求做出明智选择,并分享了实践中的有效策略与技巧。不同于传统摘要的概述方式,本文将直接以一段实践指南的形式,简述在选择自动化测试框架时应考虑的核心要素及推荐路径,旨在为读者提供即时可用的参考。 ####
|
20天前
|
机器学习/深度学习 人工智能 自然语言处理
探索软件测试的边界:从基础到高级的实践之旅
【10月更文挑战第21天】 在当今数字化时代,软件已成为我们生活和工作中不可或缺的一部分。随着技术的快速发展,对软件质量的要求也日益提高。本文旨在通过深入浅出的方式,带领读者踏上一场从基础到高级的软件测试实践之旅。我们将探讨软件测试的基本概念、重要性以及如何有效地进行测试规划和执行。通过具体案例分析,揭示常见错误及其解决方案,同时展望未来软件测试领域的发展趋势。无论你是软件开发新手还是经验丰富的测试工程师,这篇文章都将为你提供宝贵的见解和启发。
35 8
|
19天前
|
监控 安全 jenkins
探索软件测试的奥秘:自动化测试框架的搭建与实践
【10月更文挑战第24天】在软件开发的海洋里,测试是确保航行安全的灯塔。本文将带领读者揭开软件测试的神秘面纱,深入探讨如何从零开始搭建一个自动化测试框架,并配以代码示例。我们将一起航行在自动化测试的浪潮之上,体验从理论到实践的转变,最终达到提高测试效率和质量的彼岸。