from:https://www.cnblogs.com/xiadao521/p/4092846.html
上一篇介绍了数据类型转换的一些情况,可以看出,如果不进行封装,有可能导致比较混乱的代码。本文通过TDD方式把数据类型转换公共操作类开发出来,并提供源码下载。
我们在 应用程序框架实战十一:创建VS解决方案与程序集 一文已经创建了解决方案,包含一个类库项目和一个单元测试项目。单元测试将使用.Net自带的 MsTest,另外通过Resharper工具来观察测试结果。
首先考虑我们期望的API长成什么样子。基于TDD开发,其中一个作用是帮助程序员设计期望的API,这称为意图导向编程。
因为数据类型转换是Convert,所以我们先在单元测试项目中创建一个ConvertTest的类文件。
类创建好以后,我先随便创建一个方法Test,以迅速展开工作。测试的方法名Test,我是随便起的,因为现在还不清楚API是什么样,我一会再回过头来改。
using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Util.Tests { /// <summary> /// 类型转换公共操作类测试 /// </summary> [TestClass] public class ConvertTest { [TestMethod] public void Test() { } } }
为了照顾还没有使用单元测试的朋友,我在这里简单介绍一下MsTest。MsTest是.Net仿照JUnit打造的一个单元测试框架。在单元测试类上需要添加一个TestClass特性,在测试方法上添加TestMethod特性,用来识别哪些类的操作需要测试。还有一些其它特性,在用到的时候我再介绍。
现在先来实现一个最简单的功能,把字符串”1”转换为整数1。
[TestMethod] public void Test() { Assert.AreEqual( 1, Util.ConvertHelper.ToInt( "1" ) ); }
我把常用公共操作类尽量放到顶级命名空间Util,这样我就可以通过编写Util.来弹出代码提示,这样我连常用类也不用记了。
使用ConvertHelper是一个常规命名,大多数开发人员可以理解它是一个类型转换的公共操作类。我也这样用了多年,不过后面我发现Util.ConvertHelper有点啰嗦,所以我简化成Util.Convert,但Convert又和系统重名了,所以我现在使用Util.Conv,你不一定要按我的这个命名,你可以使用ConvertHelper这样的命名以提高代码清晰度。
System.Convert使用ToInt32来精确表示int是一个32位的数字,不过我们的公共操作类不用这样精确,ToInt就可以了,如果要封装ToInt64呢,我就用ToLong,这样比较符合我的习惯。
现在代码被简化成了下面的代码。
Assert.AreEqual( 1, Util.Conv.ToInt( "1" ) );
Assert在测试中用来断言,断言就是比较实际计算出来的值是否和预期一致,Assert包含大量比较方法,AreEqual使用频率最高,用来比较预期值(左边)与实际值(右边)是否值相等,还有一个AreSame方法用来比较是否引用相等。
由于Conv类还未创建,所以显示一个红色警告。 现在在Util类库项目中创建一个Conv类。
创建了Conv类以后,单元测试代码检测到Conv,但ToInt方法未创建,所以红色警告转移到ToInt方法。
现在用鼠标左键单击红色ToInit方法,Resharper在左侧显示一个红色的灯泡。
单击红色灯泡提示,选择第一项”Create Method ‘Conv.ToInt’”。
Resharper会在Conv类中自动创建一个ToInt方法。
public class Conv { public static int ToInt( string s ) { throw new NotImplementedException(); } }
方法体抛出一个未实现的异常,这正是我们想要的。TDD的口诀是“红、绿、重构”,第一步需要先保证方法执行失败,显示红色警告。至于未何需要测试先行,以及首先执行失败,牵扯TDD开发价值观,请大家参考相关资料。
准备工作已经就绪,现在可以运行测试了。安装了Resharper以后,在添加了TestClass特性的左侧,会看见两个重叠在一起的圆形图标。另外,在TestMethod特性左侧,有一个黑白相间的圆形图标。
单击Test方法左侧的图标,然后点击Run按钮。如果单击TestClass特性左侧的图标,会运行该类所有测试。
测试开始运行,并显示红色警告,提示未实现的异常,第一步完成。
为了实现功能,现在来添加ToInt方法的代码。
public static int ToInt( string s ) { int result; int.TryParse( s, out result ); return result; }
再次运行测试,已经能够成功通过,第二步完成。
第三步是进行重构,现在看哪些地方可以重构。参数s看起来有点不爽,改成data,并添加XML注释。
/// <summary> /// 转换为整型 /// </summary> /// <param name="data">数据</param> public static int ToInt( string data ) { int result; int.TryParse( data, out result ); return result; }
另外重构一下测试,为了更容易找到相关测试,一般测试文件名使用类名+Test,现在测试文件名改成ConvTest.cs,测试类名改成ConvTest。把测试方法名改成TestToInt,并添加XML注释。
/// <summary> /// 测试转换为整型 /// </summary> [TestMethod] public void TestToInt() { Assert.AreEqual( 1, Util.Conv.ToInt( "1" ) ); }
关于测试的命名,很多著作都提出了自己不同的方法。在《.Net单元测试艺术》中,作者建议使用三部分进行组合命名。还有一些著作建议将测试内容用下划线分隔单词,拼成一个长句子,以方便阅读和理解。这可能对英文水平好的人很有效,不过我的英文水平很烂,我拿一些单词拼成一个长句以后,发现更难理解了。所以我所采用的测试方法命名可能不一定好,你可以按你容易理解的方式来命名。
重构之后,需要重新测试代码,以观察是否导致失败。
上面简单介绍了TDD的一套开发流程,主要为了照顾还没有体验过单元测试的人,后面直接粘贴代码,以避免这样低效的叙述方式。
单元测试代码如下。
Conv类代码如下。
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 5 namespace Util { 6 /// <summary> 7 /// 类型转换 8 /// </summary> 9 public static class Conv { 10 11 #region 数值转换 12 13 /// <summary> 14 /// 转换为整型 15 /// </summary> 16 /// <param name="data">数据</param> 17 public static int ToInt( object data ) { 18 if ( data == null ) 19 return 0; 20 int result; 21 var success = int.TryParse( data.ToString(), out result ); 22 if ( success == true ) 23 return result; 24 try { 25 return Convert.ToInt32( ToDouble( data, 0 ) ); 26 } 27 catch ( Exception ) { 28 return 0; 29 } 30 } 31 32 /// <summary> 33 /// 转换为可空整型 34 /// </summary> 35 /// <param name="data">数据</param> 36 public static int? ToIntOrNull( object data ) { 37 if ( data == null ) 38 return null; 39 int result; 40 bool isValid = int.TryParse( data.ToString(), out result ); 41 if ( isValid ) 42 return result; 43 return null; 44 } 45 46 /// <summary> 47 /// 转换为双精度浮点数 48 /// </summary> 49 /// <param name="data">数据</param> 50 public static double ToDouble( object data ) { 51 if ( data == null ) 52 return 0; 53 double result; 54 return double.TryParse( data.ToString(), out result ) ? result : 0; 55 } 56 57 /// <summary> 58 /// 转换为双精度浮点数,并按指定的小数位4舍5入 59 /// </summary> 60 /// <param name="data">数据</param> 61 /// <param name="digits">小数位数</param> 62 public static double ToDouble( object data, int digits ) { 63 return Math.Round( ToDouble( data ), digits ); 64 } 65 66 /// <summary> 67 /// 转换为可空双精度浮点数 68 /// </summary> 69 /// <param name="data">数据</param> 70 public static double? ToDoubleOrNull( object data ) { 71 if ( data == null ) 72 return null; 73 double result; 74 bool isValid = double.TryParse( data.ToString(), out result ); 75 if ( isValid ) 76 return result; 77 return null; 78 } 79 80 /// <summary> 81 /// 转换为高精度浮点数 82 /// </summary> 83 /// <param name="data">数据</param> 84 public static decimal ToDecimal( object data ) { 85 if ( data == null ) 86 return 0; 87 decimal result; 88 return decimal.TryParse( data.ToString(), out result ) ? result : 0; 89 } 90 91 /// <summary> 92 /// 转换为高精度浮点数,并按指定的小数位4舍5入 93 /// </summary> 94 /// <param name="data">数据</param> 95 /// <param name="digits">小数位数</param> 96 public static decimal ToDecimal( object data, int digits ) { 97 return Math.Round( ToDecimal( data ), digits ); 98 } 99 100 /// <summary> 101 /// 转换为可空高精度浮点数 102 /// </summary> 103 /// <param name="data">数据</param> 104 public static decimal? ToDecimalOrNull( object data ) { 105 if ( data == null ) 106 return null; 107 decimal result; 108 bool isValid = decimal.TryParse( data.ToString(), out result ); 109 if ( isValid ) 110 return result; 111 return null; 112 } 113 114 /// <summary> 115 /// 转换为可空高精度浮点数,并按指定的小数位4舍5入 116 /// </summary> 117 /// <param name="data">数据</param> 118 /// <param name="digits">小数位数</param> 119 public static decimal? ToDecimalOrNull( object data, int digits ) { 120 var result = ToDecimalOrNull( data ); 121 if ( result == null ) 122 return null; 123 return Math.Round( result.Value, digits ); 124 } 125 126 #endregion 127 128 #region Guid转换 129 130 /// <summary> 131 /// 转换为Guid 132 /// </summary> 133 /// <param name="data">数据</param> 134 public static Guid ToGuid( object data ) { 135 if ( data == null ) 136 return Guid.Empty; 137 Guid result; 138 return Guid.TryParse( data.ToString(), out result ) ? result : Guid.Empty; 139 } 140 141 /// <summary> 142 /// 转换为可空Guid 143 /// </summary> 144 /// <param name="data">数据</param> 145 public static Guid? ToGuidOrNull( object data ) { 146 if ( data == null ) 147 return null; 148 Guid result; 149 bool isValid = Guid.TryParse( data.ToString(), out result ); 150 if ( isValid ) 151 return result; 152 return null; 153 } 154 155 /// <summary> 156 /// 转换为Guid集合 157 /// </summary> 158 /// <param name="guid">guid集合字符串,范例:83B0233C-A24F-49FD-8083-1337209EBC9A,EAB523C6-2FE7-47BE-89D5-C6D440C3033A</param> 159 public static List<Guid> ToGuidList( string guid ) { 160 var listGuid = new List<Guid>(); 161 if ( string.IsNullOrWhiteSpace( guid ) ) 162 return listGuid; 163 var arrayGuid = guid.Split( ',' ); 164 listGuid.AddRange( from each in arrayGuid where !string.IsNullOrWhiteSpace( each ) select new Guid( each ) ); 165 return listGuid; 166 } 167 168 #endregion 169 170 #region 日期转换 171 172 /// <summary> 173 /// 转换为日期 174 /// </summary> 175 /// <param name="data">数据</param> 176 public static DateTime ToDate( object data ) { 177 if ( data == null ) 178 return DateTime.MinValue; 179 DateTime result; 180 return DateTime.TryParse( data.ToString(), out result ) ? result : DateTime.MinValue; 181 } 182 183 /// <summary> 184 /// 转换为可空日期 185 /// </summary> 186 /// <param name="data">数据</param> 187 public static DateTime? ToDateOrNull( object data ) { 188 if ( data == null ) 189 return null; 190 DateTime result; 191 bool isValid = DateTime.TryParse( data.ToString(), out result ); 192 if ( isValid ) 193 return result; 194 return null; 195 } 196 197 #endregion 198 199 #region 布尔转换 200 201 /// <summary> 202 /// 转换为布尔值 203 /// </summary> 204 /// <param name="data">数据</param> 205 public static bool ToBool( object data ) { 206 if ( data == null ) 207 return false; 208 bool? value = GetBool( data ); 209 if ( value != null ) 210 return value.Value; 211 bool result; 212 return bool.TryParse( data.ToString(), out result ) && result; 213 } 214 215 /// <summary> 216 /// 获取布尔值 217 /// </summary> 218 private static bool? GetBool( object data ) { 219 switch ( data.ToString().Trim().ToLower() ) { 220 case "0": 221 return false; 222 case "1": 223 return true; 224 case "是": 225 return true; 226 case "否": 227 return false; 228 case "yes": 229 return true; 230 case "no": 231 return false; 232 default: 233 return null; 234 } 235 } 236 237 /// <summary> 238 /// 转换为可空布尔值 239 /// </summary> 240 /// <param name="data">数据</param> 241 public static bool? ToBoolOrNull( object data ) { 242 if ( data == null ) 243 return null; 244 bool? value = GetBool( data ); 245 if ( value != null ) 246 return value.Value; 247 bool result; 248 bool isValid = bool.TryParse( data.ToString(), out result ); 249 if ( isValid ) 250 return result; 251 return null; 252 } 253 254 #endregion 255 256 #region 字符串转换 257 258 /// <summary> 259 /// 转换为字符串 260 /// </summary> 261 /// <param name="data">数据</param> 262 public static string ToString( object data ) { 263 return data == null ? string.Empty : data.ToString().Trim(); 264 } 265 266 #endregion 267 268 #region 通用转换 269 270 /// <summary> 271 /// 泛型转换 272 /// </summary> 273 /// <typeparam name="T">目标类型</typeparam> 274 /// <param name="data">数据</param> 275 public static T To<T>( object data ) { 276 if ( data == null || string.IsNullOrWhiteSpace( data.ToString() ) ) 277 return default( T ); 278 Type type = Nullable.GetUnderlyingType( typeof( T ) ) ?? typeof( T ); 279 try { 280 if ( type.Name.ToLower() == "guid" ) 281 return (T)(object)new Guid( data.ToString() ); 282 if ( data is IConvertible ) 283 return (T)Convert.ChangeType( data, type ); 284 return (T)data; 285 } 286 catch { 287 return default( T ); 288 } 289 } 290 291 #endregion 292 } 293 }
Conv公共操作类的用法,在单元测试中已经说得很清楚了,这也是单元测试的一个用途,即作为API说明文档。
单元测试最强大的地方,可能是能够帮助你回归测试,如果你发现我的代码有BUG,请通知我一声,我只需要在单元测试中增加一个测试来捕获这个BUG,就可以永久修复它,并且由于采用TDD方式可以获得很高的测试覆盖率,所以我花上几秒钟运行一下全部测试,就可以知道这次修改有没有影响其它代码。这也是你创建自己的应用程序框架所必须要做的,它可以给你提供信心。
可以看到,我在单元测试中进行了很多边界测试,比如参数为null或空字符串等。但不可能穷举所有可能出错的情况,因为可能想不到,另外时间有限,也不可能做到。当在项目上发现BUG后,再通过添加单元测试的方式修复BUG就可以了。由于你的项目代码调用的是应用程序框架API,所以你只需要在框架内修复一次,项目代码完全不动。
像数据类型转换这样简单的操作,你发现写单元测试非常容易,因为它有明确的返回值,但如果没有返回值呢,甚至有外部依赖呢,那就没有这么简单了,需要很多技巧,所以你多看几本TDD和单元测试方面的著作有很多好处。
另外,再补充一下,Conv这个类里面有几个法宝。一个是ToGuidList这个方法,当你需要把字符串转换为List<Guid>的时候就用它。还有一个泛型转换的方法To<T>,很多时候可以用它进行泛型转换。
最后,我把所有方法参数类型都改成了object,主要是想使用起来方便一点,而不是只支持字符串参数,这可能导致装箱和拆箱,从而造成一些性能损失,不过我的大多数项目在性能方面还没有这么高的要求,所以这个损失对我来讲无关痛痒。
还有些数据类型转换,我没有放进来,主要是我平时很少用到,当我用到时再增加