- 优秀的单元测试不仅是保护回归,更是帮助我们设计,在编写代码前就指出代码的期望行为,从而在验证实现之前先验证设计(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);
}
- 从命名和断言,这个测试的是一个很大的方法,Mock很多外部依赖,但是其结果我们都不知道在测什么。
- 虽然这个单元测试把整个方法都覆盖了,但实际上毫无意义,更不利于今后的维护。(100%覆盖不是目标,与其追求代码覆盖率,不如将重点关注在确保写出有意义的测试)
- 测试的方法尽量小,只做一件事情,而测试也应当只测这一件事情。(函数的第一规则是要短小,第二条规则还是短小。每个函数只做一件事情(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);
}
- 根据断言可知,对Name字段的返回进行了处理,在名称前加上了Q1、Q2、一月周期的标识。
- 这个测试虽然只测了一件事情,但是测试的入口MObjectiveMapAttachDisplayBizDataInfo,该方法应该做了不止一件事情,所以Mock了一堆无用的依赖。所以我们应当把处理名称的方法单独提出(如果你先写测试,你的方法还会写成这样吗?)。
- 提出名称处理的方法HandleMobjectiveName编写单元测试。
- 以前这个原有代码这个方法时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方法测试
- 当前类下有多少private方法,如果有很多,这些私有方法属于当前类的职责吗?如果都是,是不是违反了单一职责(single Responsablity)
- 针对示例二,结合我们当前的业务代码代码层次,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);
}
- 测试代码将近200行。重复的mock参数,mock依赖占了绝大部分。
- private问题,导入测试入口过大,mock了很多无关的依赖和参数。
- 这个测试同时测了HandleWarningColor和GetColorId方法,我们应当把这两个方法分开测试。经分析,其主要逻辑在GetColorId。而HandleWarningColor方法只是构造了返回参数。
- 如果后期修改了逻辑,当前测试的断言将十分的难以维护。
重构测试
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);
}
修改后的测试更加明确,更有利于我们今后的维护。
避免复杂的私有方法
- 没什么好办法去测试private方法,因此应该尽量避免直接测试private方法。
- 如果private方法短小好记,并有助于public方法阅读,那么仅通过public方法测试他们也没有问题。
- 让你开始考虑是否需要测试的private方法时,你应该重构代码,将private方法封装的逻辑转移到另一个对象中,成为一个public方法。
- 不测试,或者祭出反射(不建议这么做。)
综上所述:了解一下TDD
- 如果你先写测试,你的方法就不会写的那么大。
- 如果你先写测试,你的测代码质量不会有那么多的重复。
- 如果你先写测试,你就不会纠结你这个方法为什么要public。
附大佬们是如何写代码的。跟熊老师学一下TDD吧。本人受益匪浅。
熊节TDD演练
网友评论