天天看點

.Net單元測試業務實踐

業務簡述

  • 關鍵字段:邀請碼最大使用次數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/

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連結。如文章對您有用,煩請點個推薦再走,感謝! 本部落格新開通打賞,滑鼠移到右側打賞浮動處,即可賞部落客點零花錢,感謝您的支援!