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

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

相关文章
|
11天前
|
测试技术 持续交付 API
深入挖掘探索.NET单元测试
【10月更文挑战第11天】
29 2
|
12天前
|
Java 测试技术 开发者
初学者入门:掌握单元测试的基础与实践
【10月更文挑战第14天】单元测试是一种软件测试方法,它验证软件中的最小可测试单元——通常是单独的函数或类——是否按预期工作。单元测试的目标是确保每个模块在其自身范围内正确无误地运行。这些测试应该独立于其他模块,并且应该能够反复执行而不受外部环境的影响。
36 2
|
20天前
|
机器学习/深度学习 人工智能 监控
提升软件质量的关键路径:高效测试策略与实践在软件开发的宇宙中,每一行代码都如同星辰般璀璨,而将这些星辰编织成星系的过程,则依赖于严谨而高效的测试策略。本文将引领读者探索软件测试的奥秘,揭示如何通过精心设计的测试方案,不仅提升软件的性能与稳定性,还能加速产品上市的步伐,最终实现质量与效率的双重飞跃。
在软件工程的浩瀚星海中,测试不仅是发现缺陷的放大镜,更是保障软件质量的坚固防线。本文旨在探讨一种高效且创新的软件测试策略框架,它融合了传统方法的精髓与现代技术的突破,旨在为软件开发团队提供一套系统化、可执行性强的测试指引。我们将从测试规划的起点出发,沿着测试设计、执行、反馈再到持续优化的轨迹,逐步展开论述。每一步都强调实用性与前瞻性相结合,确保测试活动能够紧跟软件开发的步伐,及时适应变化,有效应对各种挑战。
|
17天前
|
测试技术 UED
软件测试的艺术与实践
【10月更文挑战第9天】 在数字时代的浪潮中,软件成为了我们生活和工作不可或缺的一部分。然而,高质量的软件背后,是无数测试工程师的默默付出。本文将通过深入浅出的方式,探讨如何进行高效的软件测试,确保软件产品的质量与稳定性。我们将一起揭开软件测试的神秘面纱,从基础理论到实际操作,一步步走进这个充满挑战与创造的世界。
|
3天前
|
机器学习/深度学习 人工智能 自然语言处理
探索软件测试的边界:从基础到高级的实践之旅
【10月更文挑战第21天】 在当今数字化时代,软件已成为我们生活和工作中不可或缺的一部分。随着技术的快速发展,对软件质量的要求也日益提高。本文旨在通过深入浅出的方式,带领读者踏上一场从基础到高级的软件测试实践之旅。我们将探讨软件测试的基本概念、重要性以及如何有效地进行测试规划和执行。通过具体案例分析,揭示常见错误及其解决方案,同时展望未来软件测试领域的发展趋势。无论你是软件开发新手还是经验丰富的测试工程师,这篇文章都将为你提供宝贵的见解和启发。
19 8
|
2天前
|
监控 安全 jenkins
探索软件测试的奥秘:自动化测试框架的搭建与实践
【10月更文挑战第24天】在软件开发的海洋里,测试是确保航行安全的灯塔。本文将带领读者揭开软件测试的神秘面纱,深入探讨如何从零开始搭建一个自动化测试框架,并配以代码示例。我们将一起航行在自动化测试的浪潮之上,体验从理论到实践的转变,最终达到提高测试效率和质量的彼岸。
|
5天前
|
测试技术 C# 数据库
C# 一分钟浅谈:测试驱动开发 (TDD) 实践
【10月更文挑战第18天】测试驱动开发(TDD)是一种软件开发方法论,强调先编写测试代码再编写功能代码,以确保代码质量和可维护性。本文从 TDD 的基本概念入手,详细介绍了其核心步骤——编写测试、运行测试并失败、编写代码使测试通过,以及“红绿重构”循环。文章还探讨了 TDD 的优势,包括提高代码质量、促进设计思考、减少调试时间和文档化。此外,文中分析了常见问题及解决方案,如测试覆盖率不足、测试代码过于复杂、忽视重构和测试依赖过多,并通过一个简单的计算器类的代码案例,展示了 TDD 的实际应用过程。
13 1
|
11天前
|
测试技术 API 开发者
精通.NET单元测试:MSTest、xUnit、NUnit全面解析
【10月更文挑战第15天】本文介绍了.NET生态系统中最流行的三种单元测试框架:MSTest、xUnit和NUnit。通过示例代码展示了每种框架的基本用法和特点,帮助开发者根据项目需求和个人偏好选择合适的测试工具。
27 3
|
12天前
|
机器学习/深度学习 人工智能 自然语言处理
探索AI在软件测试中的创新应用与实践###
本文旨在探讨人工智能(AI)技术如何革新软件测试领域,提升测试效率、质量与覆盖范围。通过深入分析AI驱动的自动化测试工具、智能化缺陷预测模型及持续集成/持续部署(CI/CD)流程优化等关键方面,本研究揭示了AI技术在解决传统软件测试痛点中的潜力与价值。文章首先概述了软件测试的重要性和当前面临的挑战,随后详细介绍了AI技术在测试用例生成、执行、结果分析及维护中的应用实例,并展望了未来AI与软件测试深度融合的趋势,强调了技术伦理与质量控制的重要性。本文为软件开发与测试团队提供了关于如何有效利用AI技术提升测试效能的实践指南。 ###
|
21天前
|
测试技术
软件测试中的探索性测试(ET)实践
【10月更文挑战第5天】本文将深入探讨一种与传统脚本化测试不同的测试方法——探索性测试(Exploratory Testing,简称ET)。我们将通过一个实际案例来展示ET的有效性,并分享如何将ET融入日常的软件测试流程中。文章旨在为测试人员提供一种灵活、高效的测试策略,帮助他们更好地发现软件中的缺陷。