美文网首页敏捷教练
有效的单元测试

有效的单元测试

作者: 月月星星臭臭居居 | 来源:发表于2020-01-11 19:21 被阅读0次

    图书推荐:有效的单元测试重构

    • 优秀的单元测试不仅是保护回归,更是帮助我们设计,在编写代码前就指出代码的期望行为,从而在验证实现之前先验证设计(TDD)。
    • 我们现在需要编写单元测试,更要编写好的单元测试。不好的单元测试,例如缺乏可读性、测试结果准确度低等问题,会导致降低你的分析速度,同时在修改代码后,导致单元测试难以维护,修改者不仅要改代码逻辑还需维护修改后的单元测试。 造成双重打击。
    • 优秀的单元测试应具备:可读性、可维护性、可信赖
    • 19年下半年,公司要求单元测试覆盖度50%,所以大家开始了单元测试的维护和编写。而最近修改了一个简单的代码逻辑,当加载列表数据时,需要验证权限问题。当我改完这一行代码,5-6个单元测试飘红。当我再去维护单元测试时,苦不堪言。其根本无从下手。结合示列我们来引申一下关于单元测试的一系列问题。

    问题一:保证单元测试的正确性

    超长的测试使读者不得不猜想每段代码出现的理由,以及那段代码要测什么。每个测试函数中应当只测试一个概念。
    要点只能是小,测试的功能小到一目了然,不需要去证明测试的准确性。

    示例一:(可读性差、难以维护、断言不明确)

    [TestMethod]
    public void GetSubObjectiveAndAttachInfo_Should_Return_True_Test()
    {
        var superId = Guid.NewGuid();
        _filterBuilderService.Setup(p => p.BuildMapSubObjectiveFilter(tenantId, userId, It.IsAny()))
            .Returns(new BooleanFilter());
        _mObjectiveDao.Setup(p => p.Query(tenantId, It.IsAny(), null, null))
            .Returns(new List() {
                        new MObjective()
                        {
                            ObjectId = superId,
                            OKRType =1,
                            StartTime = DateTime.Now,
                            EndTime = DateTime.Now
                        }
            });
        _permissionService.Setup(p => p.CheckMObjectiveAdminRight(tenantId, userId, It.IsAny > ()))
            .Returns(new Dictionary() { { superId, true } });
        _permissionService.Setup(p => p.CheckMObjectiveUserRightById(tenantId, userId, It.IsAny > (), true))
            .Returns(new Dictionary());
        _staffService.Setup(p => p.GetStaffOrgName(It.IsAny(), It.IsAny(), It.IsAny > ())).Returns(value: new Dictionary() { { 2, "Test" } });
        _mPeriodService.Setup(p => p.GetPeriodValueDic(It.IsAny(), false)).Returns(value: new Dictionary() { { 1, new MPeriod() { Name = "一月" } } });
        _mPeriodService.Setup(p => p.GetDefaultPeriodValueName(It.IsAny())).Returns(value: new Dictionary(new Dictionary()));
        _mTargetSettingService.Setup(p => p.GetWarningColorTypesConfig(It.IsAny())).Returns(value: new List());
        _permissionService.Setup(p => p.CheckMObjectiveUserOrAdminRightById(tenantId, userId, superId)).Returns(true);
        _permissionService.Setup(p => p.CheckSubObjectivesUserAuthCountForMap(tenantId, userId, It.IsAny > ())).Returns(new Dictionary());
        _titaApiService.Setup(p => p.GetUserAvatarInfoById(TenantId, UserId, It.IsAny())).Returns(new Model.UIModel.OwnerAvatar());
        _mClassifyDao.Setup(p => p.Get(TenantId, It.IsAny > (), false)).Returns(new List());
        _userInterfaceV2Service.Setup(p => p.GetListV2Data(MObjectiveConst.MObjectiveList.MBOAllPropertyList, It.IsAny(), MObjectiveConst.ObjectName, ""))
        .Returns(new List> ()
                {
            new Dictionary
                    {
                        { "_id", new FieldBizData(){Name = "_id", Value = superId.ToString() } },
                        { "OwnerId", new FieldBizData(){Name = "OKRType", Value = "1" } },
                        { "SuperId", new FieldBizData(){Name = "SuperId", Value = Guid.Empty.ToString() } },
                        { "StartTime", new FieldBizData(){Name = "StartTime", Value =DateTime.Now.ToString() } },
                        { "EndTime", new FieldBizData(){Name = "EndTime", Value = DateTime.Now.ToString() } },
                    }
                });
        var service = TestAutofacContext.ResolveByProperty(_filterBuilderService.Object, _mObjectiveDao.Object, _permissionService.Object, _staffService.Object,
            _mPeriodService.Object, _mTargetSettingService.Object, _titaApiService.Object, _mClassifyDao.Object, _userInterfaceV2Service.Object);
        Assert.AreEqual(200, service.GetSubObjectiveAndAttachInfo(tenantId, userId, superId.ToString()).Code);
    }
    
    1. 从命名和断言,这个测试的是一个很大的方法,Mock很多外部依赖,但是其结果我们都不知道在测什么。
    2. 虽然这个单元测试把整个方法都覆盖了,但实际上毫无意义,更不利于今后的维护。(100%覆盖不是目标,与其追求代码覆盖率,不如将重点关注在确保写出有意义的测试)
    3. 测试的方法尽量小,只做一件事情,而测试也应当只测这一件事情。(函数的第一规则是要短小,第二条规则还是短小。每个函数只做一件事情(Clean Code)。 不仅便于理解,更有利于单元测试的编写。)

    问题二:保持测试整洁

    有些人认为,测试环境代码的维护不应遵循生产代码的质量标准。变量命名不规范,测试函数缺乏描述性,测试代码缺乏设计。其原因包含:

    • 对单元测试的重视不够。
    • 其次是对只是一味考虑覆盖度,并未真正思考过测试的意义。
    • 先写的代码,后补的单元测。这时发现单元测试很难编写,但是又不愿意去改变原有代码的逻辑。(为什么说单元测试是有助于我们设计,与其这样不如先写测试)

    测试代码和生产代码一样重要。他不是二等公民。他需要被思考、被设计和被照料。他应该像生产代码一般保持整洁。

    示列二(重复代码)

    [TestMethod]
    public void MObjectiveMapAttachDisplayBizDataInfo_Should_Name_Match_When_Period_Test()
    {
        var superId = Guid.NewGuid();
        var objs = new MObjective[5]
        {
                    new MObjective()
                    {
                        Name = "test",
                        Period = 21,
                        ObjectId = Guid.NewGuid(),
                        StartTime = DateTime.Now,
                        EndTime = DateTime.Now.AddDays(1)
                    },
                    new MObjective()
                    {
                        Name = "test2",
                        Period = 22,
                        ObjectId = Guid.NewGuid(),
                        StartTime = DateTime.Now,
                        EndTime = DateTime.Now.AddDays(1)
                    },
                    new MObjective()
                    {
                        Name = "test3",
                        Period = 23,
                        ObjectId = Guid.NewGuid(),
                        StartTime = DateTime.Now,
                        EndTime = DateTime.Now.AddDays(1)
                    },
                    new MObjective()
                    {
                        Name = "test4",
                        Period = 24,
                        ObjectId = Guid.NewGuid(),
                        StartTime = DateTime.Now,
                        EndTime = DateTime.Now.AddDays(1)
                    },
                    new MObjective()
                    {
                        Name = "test5",
                        Period = 1,
                        ObjectId = Guid.NewGuid(),
                        StartTime = DateTime.Now,
                        EndTime = DateTime.Now.AddDays(1)
                    }
        };
        _staffService.Setup(p => p.GetStaffOrgName(It.IsAny(), It.IsAny(), It.IsAny > ())).Returns(value: new Dictionary() { { 2, "Test" } });
        _mPeriodService.Setup(p => p.GetPeriodValueDic(It.IsAny(), false)).Returns(value: new Dictionary() { { 1, new MPeriod() { Name = "一月" } } });
        _mPeriodService.Setup(p => p.GetNameByPValue(It.IsAny(), 1)).Returns("一月");
        _mPeriodService.Setup(p => p.GetDefaultPeriodValueName(It.IsAny())).Returns(value: new Dictionary(new Dictionary() { { 1, "一月" } }));
        _mTargetSettingService.Setup(p => p.GetWarningColorTypesConfig(It.IsAny())).Returns(value: new List());
        _permissionService.Setup(p => p.CheckMObjectiveUserOrAdminRightById(TenantId, UserId, superId)).Returns(true);
        _permissionService.Setup(p => p.CheckSubObjectivesUserAuthCountForMap(TenantId, UserId, It.IsAny())).Returns(new Dictionary());
        _titaApiService.Setup(p => p.GetUserAvatarInfoById(TenantId, UserId, It.IsAny())).Returns(new Model.UIModel.OwnerAvatar());
        _mClassifyDao.Setup(p => p.Get(TenantId, It.IsAny > (), false)).Returns(new List());
        _userInterfaceV2Service.Setup(p => p.GetListV2Data(MObjectiveConst.MObjectiveList.MBOAllPropertyList, It.IsAny(), MObjectiveConst.ObjectName, "")).Returns(new List> ()
                {
            new Dictionary
                    {
                        { "_id", new FieldBizData(){Name = "_id", Value = objs[0].ObjectId.ToString() } },
                        { "OwnerId", new FieldBizData(){Name = "OwnerId", Value = "1" } },
                        { "SuperId", new FieldBizData(){Name = "SuperId", Value = Guid.Empty.ToString() } },
                        { "StartTime", new FieldBizData(){Name = "StartTime", Value =objs[0].StartTime.ToString() } },
                        { "EndTime", new FieldBizData(){Name = "EndTime", Value = objs[0].EndTime.ToString() } },
                        { "Period", new FieldBizData(){Name = "Period", Value = objs[0].Period.ToString() } },
                        { "Name", new FieldBizData(){Name = "Name", Value = objs[0].Name.ToString() } },
                    },
                     new Dictionary
                    {
                        { "_id", new FieldBizData(){Name = "_id", Value = objs[1].ObjectId.ToString() } },
                        { "OwnerId", new FieldBizData(){Name = "OwnerId", Value = "2" } },
                        { "SuperId", new FieldBizData(){Name = "SuperId", Value = Guid.Empty.ToString() } },
                        { "StartTime", new FieldBizData(){Name = "StartTime", Value =objs[0].StartTime.ToString() } },
                        { "EndTime", new FieldBizData(){Name = "EndTime", Value = objs[0].EndTime.ToString() } },
                        { "Period", new FieldBizData(){Name = "Period", Value = objs[1].Period.ToString() } },
                        { "Name", new FieldBizData(){Name = "Name", Value = objs[1].Name.ToString() } },
                    }, new Dictionary
                    {
                        { "_id", new FieldBizData(){Name = "_id", Value = objs[2].ObjectId.ToString() } },
                        { "OwnerId", new FieldBizData(){Name = "OwnerId", Value = "2" } },
                        { "SuperId", new FieldBizData(){Name = "SuperId", Value = Guid.Empty.ToString() } },
                        { "StartTime", new FieldBizData(){Name = "StartTime", Value =objs[0].StartTime.ToString() } },
                        { "EndTime", new FieldBizData(){Name = "EndTime", Value = objs[0].EndTime.ToString() } },
                        { "Period", new FieldBizData(){Name = "Period", Value = objs[2].Period.ToString() } },
                        { "Name", new FieldBizData(){Name = "Name", Value = objs[2].Name.ToString() } },
                    }, new Dictionary
                    {
                        { "_id", new FieldBizData(){Name = "_id", Value = objs[3].ObjectId.ToString() } },
                        { "OwnerId", new FieldBizData(){Name = "OwnerId", Value = "2" } },
                        { "SuperId", new FieldBizData(){Name = "SuperId", Value = Guid.Empty.ToString() } },
                        { "StartTime", new FieldBizData(){Name = "StartTime", Value =objs[0].StartTime.ToString() } },
                        { "EndTime", new FieldBizData(){Name = "EndTime", Value = objs[0].EndTime.ToString() } },
                        { "Period", new FieldBizData(){Name = "Period", Value = objs[3].Period.ToString() } },
                        { "Name", new FieldBizData(){Name = "Name", Value = objs[3].Name.ToString() } },
                    }, new Dictionary
                    {
                        { "_id", new FieldBizData(){Name = "_id", Value = objs[4].ObjectId.ToString() } },
                        { "OwnerId", new FieldBizData(){Name = "OwnerId", Value = "2" } },
                        { "SuperId", new FieldBizData(){Name = "SuperId", Value = Guid.Empty.ToString() } },
                        { "StartTime", new FieldBizData(){Name = "StartTime", Value =objs[0].StartTime.ToString() } },
                        { "EndTime", new FieldBizData(){Name = "EndTime", Value = objs[0].EndTime.ToString() } },
                        { "Period", new FieldBizData(){Name = "Period", Value = objs[4].Period.ToString() } },
                        { "Name", new FieldBizData(){Name = "Name", Value = objs[4].Name.ToString() } },
                    },
                });
        var service = TestAutofacContext.ResolveByProperty(_userInterfaceV2Service.Object, _staffService.Object, _permissionService.Object, _mPeriodService.Object, _mTargetSettingService.Object,
            _titaApiService.Object, _mClassifyDao.Object, _userInterfaceV2Service.Object);
    
        var result = service.MObjectiveMapAttachDisplayBizDataInfo(TenantId, UserId, objs);
    
        Assert.AreEqual("【Q1】test", result[0]["Name"].Value);
        Assert.AreEqual("【Q2】test2", result[1]["Name"].Value);
        Assert.AreEqual("【Q3】test3", result[2]["Name"].Value);
        Assert.AreEqual("【Q4】test4", result[3]["Name"].Value);
        Assert.AreEqual("【一月】test5", result[4]["Name"].Value);
    }
    
    1. 根据断言可知,对Name字段的返回进行了处理,在名称前加上了Q1、Q2、一月周期的标识。
    2. 这个测试虽然只测了一件事情,但是测试的入口MObjectiveMapAttachDisplayBizDataInfo,该方法应该做了不止一件事情,所以Mock了一堆无用的依赖。所以我们应当把处理名称的方法单独提出(如果你先写测试,你的方法还会写成这样吗?)。
    3. 提出名称处理的方法HandleMobjectiveName编写单元测试。
    4. 以前这个原有代码这个方法时private,我们可以改为public,或者考虑反射(不赞同)。 private原因我认为是我们后期调整代码结构Extract Method的时候,VS自动设成private的。如果是考虑的是封装性,我们后面再谈)

    单元测试优化

    Step1:拆分断言

    [TestMethod]
    public void Should_Name_StartWith_Q1_When_Period_FirstQuarter()
    {
        Dictionary bizData = new Dictionary();
        MObjective mapModel = new MObjective() { Period = 21, Name = "积极向上" };
        var service = new MObjectiveMapService();
        service.HandleMobjectiveName(tenantId, bizData, mapModel);
        Assert.AreEqual("【Q1】积极向上", bizData["Name"].Value);
    }
    
    [TestMethod]
    public void Should_Name_StartWith_Q2_When_Period_SecondQuarter()
    {
        Dictionary bizData = new Dictionary();
        MObjective mapModel = new MObjective() { Period = 22, Name = "积极向上" };
        var service = new MObjectiveMapService();
        service.HandleMobjectiveName(tenantId, bizData, mapModel);
        Assert.AreEqual("【Q2】积极向上", bizData["Name"].Value);
    }
    

    Step2:发现很多重复代码,重构,提取公共断言方法,测试,通过后,再补充剩余的条件。

    [TestMethod]
    public void Should_Name_StartWith_Q1_When_Period_FirstQuarter()
    {
        AssertObjectiveName(21, "Q1");
    }
    
    [TestMethod]
    public void Should_Name_StartWith_Q2_When_Period_SecondQuarter()
    {
        AssertObjectiveName(22, "Q2");
    }
    
    [TestMethod]
    public void Should_Name_StartWith_1_When_Period_January()
    {
        AssertObjectiveName(1, "1月");
    }
    
    private void AssertObjectiveName(int period, string expectedName)
    {
        Dictionary<string, FieldBizData> bizData = new Dictionary<string, FieldBizData>();
        MObjective mapModel = new MObjective() { Period = period, Name = "积极向上" };
        var service = new MObjectiveMapService()
        {
            MPeriodService = Mock.Of<IMPeriodService>(x => x.GetNameByPValue(It.IsAny<int>(), 1) == expectedName)
        };
        service.HandleMobjectiveName(tenantId, bizData, mapModel);
        Assert.AreEqual($"【{expectedName}】积极向上", bizData["Name"].Value);
    }
    

    总结下:修改后,我们明确了每一个测试方法的目的,不到30行,其实就解决了最初的100多行的代码。再次验证了单元测试有助于我们设计。


    问题三 :private方法测试

    1. 当前类下有多少private方法,如果有很多,这些私有方法属于当前类的职责吗?如果都是,是不是违反了单一职责(single Responsablity)
    2. 针对示例二,结合我们当前的业务代码代码层次,service层作为业务逻辑层,本身就不OO,在这里纠结一个方法的修饰符意义并不是很大。

    private方法长到让你开始考虑是否需要测试时,他就应该去他自己的对象。否则,你就不测试他,或者祭出反射(不建议这么做。)

    示例三:(测试方法为private,缺乏可读性、断言不明确、重复)

    我们先看下业务逻辑代码

    /// <summary>
    /// -HeadBgColor目标标头背景色, MouseOverColor目标空白区背景色, ProgressBgColor进度条背景色, ProgressFrontColor进度条前景色
    /// </summary>
    /// <param name="tenantId"></param>
    /// <param name="userId"></param>
    /// <param name="bizData"></param>
    /// <param name="mapModel"></param>
    /// <param name="colorItems"></param>
    private void HandleWarningColor(int tenantId, int userId, Dictionary<string, FieldBizData> bizData, MObjective mapModel, List<WarningColorType> colorItems)
    {
        var id = GetColorId(mapModel.StartTime, mapModel.EndTime, mapModel.OStatus, mapModel.Progress);
        var colorItem = colorItems.FirstOrDefault(p => p.Id == id.ToInt());
        if (colorItem != null)
        {
            bizData.SetBizData("HeadBgColor", colorItem.HeadBgColor);
            bizData.SetBizData("MouseOverColor", colorItem.MouseOverColor);
            bizData.SetBizData("ProgressBgColor", colorItem.ProgressBgColor);
            bizData.SetBizData("ProgressFrontColor", colorItem.ProgressFrontColor);
            bizData.SetBizData("WarningColorId", colorItem.Id.ToString());
        }
        else
        {
            bizData.SetBizData("HeadBgColor", "");
            bizData.SetBizData("MouseOverColor", "");
            bizData.SetBizData("ProgressBgColor", "");
            bizData.SetBizData("ProgressFrontColor", "");
            bizData.SetBizData("WarningColorId", "");
            LogManager.Error(tenantId, userId, $"目标地图未知的标识颜色警示值: {id.ToInt()}");
        }
    }
    
    /// <summary>
    /// 根据目标属性获取目标的颜色警示颜色
    /// </summary>
    /// <param name="startTime">开始时间</param>
    /// <param name="endTime">结束时间</param>
    /// <param name="status">目标状态状态</param>
    /// <param name="progress">进度</param>
    /// <returns></returns>
    private WarningColorEnum GetColorId(DateTime startTime, DateTime endTime, int status, double progress)
    {
        if (status == (int)MObjectiveStatus.Finished)
        {
            return WarningColorEnum.Finished;//blue
        }
        if (status == (int)MObjectiveStatus.Closed)
        {
            return WarningColorEnum.Closed;//grey
        }
        //跟前端保持一样的逻辑
        var startTimestamp = startTime.GetMillisecondTimestamp();
        var endTimestamp = endTime.GetMillisecondTimestamp();
        var currentTimestamp = DateTime.Now.GetMillisecondTimestamp();
        var timeProgressNum = endTimestamp - startTimestamp == 0 ? int.MaxValue : Convert.ToInt32(Convert.ToDouble(currentTimestamp - startTimestamp) / (endTimestamp - startTimestamp) * 100);
        timeProgressNum = timeProgressNum >= 100 ? 100 : timeProgressNum;
        timeProgressNum = currentTimestamp - startTimestamp <= 0 ? 0 : timeProgressNum;
        var diff = timeProgressNum - Convert.ToInt64(progress);
        if (diff > 30)
        {
            return WarningColorEnum.HighRisk; //red
        }
        if (diff < 10)
        {
            return WarningColorEnum.LowRisk; //green
        }
        return WarningColorEnum.MediumRisk; //yellow
    }
    

    业务逻辑代码也就50来行。我们再看下测试代码

    [TestMethod]
    public void MObjectiveMapAttachDisplayBizDataInfo_Should_Have_Color_When_ColorItem_IsNot_Null_Test()
    {
        var superId = Guid.NewGuid();
        var objs = new MObjective[5] { new MObjective()
                {
                    OwnerId = 1,
                    SuperId = Guid.Empty,
                    ObjectId = Guid.Empty,
                    StartTime = DateTime.Now,
                    EndTime = DateTime.Now,
                    OStatus = 2
                },
                new MObjective(){
                    ObjectId = Guid.NewGuid(),
                    OwnerId = 2,
                    SuperId =superId,
                    OId = superId,
                    StartTime = DateTime.Now,
                    EndTime = DateTime.Now,
                    OStatus = 3
                },
                new MObjective()
                {
                    ObjectId = Guid.NewGuid(),
                    OwnerId=3,
                    SuperId = Guid.Empty,
                    StartTime = DateTime.Now,
                    EndTime = DateTime.Now,
                    OStatus = 1
                },
                new MObjective()
                {
                    ObjectId = Guid.NewGuid() ,
                    OwnerId =3,
                    SuperId = Guid.Empty,
                    OStatus = 1,
                    StartTime = DateTime.Now,
                    EndTime = DateTime.Now,
                    Progress = 95
                },
                new MObjective()
                {
                    ObjectId = Guid.NewGuid() ,
                    OwnerId =3,
                    SuperId = Guid.Empty,
                    OStatus = 1,
                    StartTime = DateTime.Now,
                    EndTime = DateTime.Now,
                    Progress = 80
                }
                };
    
        var subObjectivesUserAuthCountForMap = new Dictionary<Guid, int>();
        subObjectivesUserAuthCountForMap.Add(superId, 1);
    
        _staffService.Setup(p => p.GetStaffOrgName(It.IsAny<int>(), It.IsAny<int>(), It.IsAny<IEnumerable<int>>())).Returns(value: new Dictionary<int, string>());
        _mPeriodService.Setup(p => p.GetPeriodValueDic(It.IsAny<int>(), false)).Returns(value: new Dictionary<int, MPeriod>());
        _mPeriodService.Setup(p => p.GetDefaultPeriodValueName(It.IsAny<int>())).Returns(value: new Dictionary<int, string>(new Dictionary<int, string>()));
        _mTargetSettingService.Setup(p => p.GetWarningColorTypesConfig(It.IsAny<int>()))
            .Returns(
                new List<Utility.WarningColorType>() {
                            new Utility.WarningColorType()
                            {
                                Id =1,
                                HeadBgColor = "blue",
                                MouseOverColor = "blue",
                                ProgressBgColor = "blue",
                                ProgressFrontColor = "blue",
                            },
                            new Utility.WarningColorType()
                            {
                                Id=0,
                                HeadBgColor = "grey",
                                MouseOverColor = "grey",
                                ProgressBgColor = "grey",
                                ProgressFrontColor = "grey",
                            },
                            new Utility.WarningColorType()
                            {
                                Id =4,
                                HeadBgColor = "red",
                                MouseOverColor = "red",
                                ProgressBgColor = "red",
                                ProgressFrontColor = "red",
                            },
                            new Utility.WarningColorType()
                            {
                                Id =2,
                                HeadBgColor = "green",
                                MouseOverColor = "green",
                                ProgressBgColor = "green",
                                ProgressFrontColor = "green",
                            },
                            new Utility.WarningColorType()
                            {
                                Id =3,
                                HeadBgColor = "yellow",
                                MouseOverColor = "yellow",
                                ProgressBgColor = "yellow",
                                ProgressFrontColor = "yellow",
                            }
                }
            );
        _permissionService.Setup(p => p.CheckMObjectiveUserOrAdminRightById(TenantId, UserId, superId)).Returns(true);
        _permissionService.Setup(p => p.CheckSubObjectivesUserAuthCountForMap(TenantId, UserId, It.IsAny<IEnumerable<Guid>>())).Returns(subObjectivesUserAuthCountForMap);
        _titaApiService.Setup(p => p.GetUserAvatarInfoById(TenantId, UserId, It.IsAny<int>())).Returns(new Model.UIModel.OwnerAvatar());
        _mClassifyDao.Setup(p => p.Get(TenantId, It.IsAny<IEnumerable<Guid>>(), false)).Returns(new List<MClassify>());
        _mObjectiveDao.Setup(p => p.Get(TenantId, It.IsAny<Guid>(), false)).Returns(new MObjective() { Name = "" });
    
        _userInterfaceV2Service.Setup(p => p.GetListV2Data(MObjectiveConst.MObjectiveList.MBOAllPropertyList, It.IsAny<SearchCondition>(), MObjectiveConst.ObjectName, "")).Returns(new List<Dictionary<string, FieldBizData>>()
                {
                    new Dictionary<string, FieldBizData>
                    {
                        { "_id", new FieldBizData(){Name = "_id", Value = objs[0].ObjectId.ToString() } },
                        { "OwnerId", new FieldBizData(){Name = "OwnerId", Value = "1" } },
                        { "SuperId", new FieldBizData(){Name = "SuperId", Value = Guid.Empty.ToString() } },
                        { "StartTime", new FieldBizData(){Name = "StartTime", Value =objs[0].StartTime.ToString() } },
                        { "EndTime", new FieldBizData(){Name = "EndTime", Value = objs[0].EndTime.ToString() } },
                        { "OStatus", new FieldBizData(){Name = "OStatus", Value = "2" } },
                    },
                    new Dictionary<string, FieldBizData>
                    {
                        { "_id", new FieldBizData(){Name = "_id", Value = objs[1].ObjectId.ToString() } },
                        { "SuperId",new FieldBizData() { Name ="SuperId", Value = superId.ToString() } },
                        { "OwnerId", new FieldBizData(){Name = "OwnerId", Value = "2" } },
                        { "StartTime", new FieldBizData(){Name = "StartTime", Value =objs[1].StartTime.ToString() } },
                        { "EndTime", new FieldBizData(){Name = "EndTime", Value = objs[1].EndTime.ToString() } },
                        { "OStatus", new FieldBizData(){Name = "OStatus", Value = "3" } },
                    },new Dictionary<string, FieldBizData>
                    {
                        { "_id", new FieldBizData(){Name = "_id", Value = objs[2].ObjectId.ToString() } },
                        { "SuperId",new FieldBizData() { Name ="SuperId", Value = Guid.Empty.ToString() } },
                        { "OwnerId", new FieldBizData(){Name = "OwnerId", Value = "3" } },
                        { "StartTime", new FieldBizData(){Name = "StartTime", Value =objs[2].StartTime.ToString() } },
                        { "EndTime", new FieldBizData(){Name = "EndTime", Value = objs[2].EndTime.ToString() } },
                        { "OStatus", new FieldBizData(){Name = "OStatus", Value = "1" } },
                    },new Dictionary<string, FieldBizData>
                    {
                        { "_id", new FieldBizData(){Name = "_id", Value = objs[3].ObjectId.ToString() } },
                        { "SuperId",new FieldBizData() { Name ="SuperId", Value = Guid.Empty.ToString() } },
                        { "OwnerId", new FieldBizData(){Name = "OwnerId", Value = "3" } },
                        { "StartTime", new FieldBizData(){Name = "StartTime", Value =objs[3].StartTime.ToString() } },
                        { "EndTime", new FieldBizData(){Name = "EndTime", Value = objs[3].EndTime.ToString() } },
                        { "Progress", new FieldBizData(){Name = "Progress", Value = "95" } },
                    },new Dictionary<string, FieldBizData>
                    {
                        { "_id", new FieldBizData(){Name = "_id", Value = objs[4].ObjectId.ToString() } },
                        { "SuperId",new FieldBizData() { Name ="SuperId", Value = Guid.Empty.ToString() } },
                        { "OwnerId", new FieldBizData(){Name = "OwnerId", Value = "3" } },
                        { "StartTime", new FieldBizData(){Name = "StartTime", Value =objs[4].StartTime.ToString() } },
                        { "EndTime", new FieldBizData(){Name = "EndTime", Value = objs[4].EndTime.ToString() } },
                        { "OStatus", new FieldBizData(){Name = "OStatus", Value = "1" } },
                        { "Progress", new FieldBizData(){Name = "Progress", Value = "80" } },
                    },
                });
        var service = TestAutofacContext.ResolveByProperty<IMObjectiveMapService>(_userInterfaceV2Service.Object, _staffService.Object, _permissionService.Object, _mPeriodService.Object, _mTargetSettingService.Object,
            _titaApiService.Object, _mClassifyDao.Object, _mObjectiveDao.Object);
    
        var result = service.MObjectiveMapAttachDisplayBizDataInfo(tenantId, userId, objs);
        Assert.AreEqual("blue", result[0]["HeadBgColor"].Value);
        Assert.AreEqual("blue", result[0]["MouseOverColor"].Value);
        Assert.AreEqual("blue", result[0]["ProgressBgColor"].Value);
        Assert.AreEqual("blue", result[0]["ProgressFrontColor"].Value);
    
        Assert.AreEqual("grey", result[1]["HeadBgColor"].Value);
        Assert.AreEqual("grey", result[1]["MouseOverColor"].Value);
        Assert.AreEqual("grey", result[1]["ProgressBgColor"].Value);
        Assert.AreEqual("grey", result[1]["ProgressFrontColor"].Value);
    
        Assert.AreEqual("red", result[2]["HeadBgColor"].Value);
        Assert.AreEqual("red", result[2]["MouseOverColor"].Value);
        Assert.AreEqual("red", result[2]["ProgressBgColor"].Value);
        Assert.AreEqual("red", result[2]["ProgressFrontColor"].Value);
    
        Assert.AreEqual("green", result[3]["HeadBgColor"].Value);
        Assert.AreEqual("green", result[3]["MouseOverColor"].Value);
        Assert.AreEqual("green", result[3]["ProgressBgColor"].Value);
        Assert.AreEqual("green", result[3]["ProgressFrontColor"].Value);
    
        Assert.AreEqual("yellow", result[4]["HeadBgColor"].Value);
        Assert.AreEqual("yellow", result[4]["MouseOverColor"].Value);
        Assert.AreEqual("yellow", result[4]["ProgressBgColor"].Value);
        Assert.AreEqual("yellow", result[4]["ProgressFrontColor"].Value);
    }
    
    1. 测试代码将近200行。重复的mock参数,mock依赖占了绝大部分。
    2. private问题,导入测试入口过大,mock了很多无关的依赖和参数。
    3. 这个测试同时测了HandleWarningColor和GetColorId方法,我们应当把这两个方法分开测试。经分析,其主要逻辑在GetColorId。而HandleWarningColor方法只是构造了返回参数。
    4. 如果后期修改了逻辑,当前测试的断言将十分的难以维护。

    重构测试
    Step1:GetColorId 测试

    [TestMethod()]
    public void Should_Finished_Blue_Color_When_Status_Finished()
    {
        AssertGetColor(WarningColorEnum.Finished, new DateTime(2019, 1, 1), new DateTime(2019, 1, 31), MObjectiveStatus.Finished.ToInt());
    }
    
    [TestMethod()]
    public void Should_Closed_Grey_Color_When_Status_Finished()
    {
        AssertGetColor(WarningColorEnum.Closed, new DateTime(2019, 1, 1), new DateTime(2019, 1, 31), MObjectiveStatus.Closed.ToInt());
    }
    
    [TestMethod()]
    public void Should_HighRisk_Red_Color()
    {
        AssertGetColor(WarningColorEnum.HighRisk, new DateTime(2019, 1, 1), new DateTime(2019, 1, 31), MObjectiveStatus.All.ToInt(), 69);
    }
    
    [TestMethod()]
    public void Should_MediumRisk_Yellow_Color()
    {
        AssertGetColor(WarningColorEnum.MediumRisk, new DateTime(2019, 1, 1), new DateTime(2019, 1, 31), MObjectiveStatus.All.ToInt(), 83);
    }
    
    [TestMethod()]
    public void Should_LowRisk_Green_Color()
    {
        AssertGetColor(WarningColorEnum.LowRisk, new DateTime(2019, 1, 1), new DateTime(2019, 1, 31), MObjectiveStatus.All.ToInt(), 91);
    }
    
    private static void AssertGetColor(WarningColorEnum expected, DateTime startTime, DateTime dateTime, int status = 0, int progress = 0)
    {
        var service = new MObjectiveMapService();
        var result = service.GetColorId(startTime, dateTime, status, progress);
        Assert.AreEqual(expected, result);
    }
    

    Setp2:

    [TestMethod]
    public void Should_Verify_Color_Setting_When_HighRisk()
    {
        var highRiskColorType = new WarningColorType()
        {
            Id = 4,
            HeadBgColor = "red",
            MouseOverColor = "green",
            ProgressBgColor = "blue",
            ProgressFrontColor = "black",
        };
        MObjective mapModel = new MObjective()
        {
            StartTime = new DateTime(2019, 1, 1),
            EndTime = new DateTime(2019, 1, 31)
                                                         ,
            OStatus = MObjectiveStatus.InProgress.ToInt(),
            Progress = 10
        };
        var service = new MObjectiveMapService();
        var result = new Dictionary<string, FieldBizData>();
        service.HandleWarningColor(
            tenantId, userId, result, mapModel,
            new List<WarningColorType> { highRiskColorType });
        Assert.AreEqual("red", result["HeadBgColor"].Value);
        Assert.AreEqual("green", result["MouseOverColor"].Value);
        Assert.AreEqual("blue", result["ProgressBgColor"].Value);
        Assert.AreEqual("black", result["ProgressFrontColor"].Value);
        Assert.AreEqual("4", result["WarningColorId"].Value);
    }
    

    修改后的测试更加明确,更有利于我们今后的维护。

    避免复杂的私有方法

    1. 没什么好办法去测试private方法,因此应该尽量避免直接测试private方法。
    2. 如果private方法短小好记,并有助于public方法阅读,那么仅通过public方法测试他们也没有问题。
    3. 让你开始考虑是否需要测试的private方法时,你应该重构代码,将private方法封装的逻辑转移到另一个对象中,成为一个public方法。
    4. 不测试,或者祭出反射(不建议这么做。)

    综上所述:了解一下TDD

    • 如果你先写测试,你的方法就不会写的那么大。
    • 如果你先写测试,你的测代码质量不会有那么多的重复。
    • 如果你先写测试,你就不会纠结你这个方法为什么要public。

    附大佬们是如何写代码的。跟熊老师学一下TDD吧。本人受益匪浅。
    熊节TDD演练

    相关文章

      网友评论

        本文标题:有效的单元测试

        本文链接:https://www.haomeiwen.com/subject/nwkbactx.html