单元测试——如何将对象序列化为c#对象初始化代码
本文关键字:对象 初始化 代码 序列化 单元测试 | 更新日期: 2023-09-27 17:52:56
我正在寻找一个内存中的对象(或对象的JSON序列化),并发出c#代码来产生一个等效的对象。
这对于从存储库中提取已知的良好示例以用作单元测试的起点非常有用。我们考虑过对JSON进行反序列化,但是c#代码在重构方面更有优势。
有一个有趣的Visual Studio扩展可以解决这个问题;对象导出器。它允许将内存中的对象序列化为c#对象初始化代码、JSON和XML。我还没有试过,但看起来很有趣;将更新后尝试它。
如果你的模型很简单,你可以使用反射和字符串生成器直接输出c#。我这样做是为了完全按照您所讨论的那样填充单元测试数据。
下面的代码示例是在几分钟内编写的,并生成了一个需要手工调整的对象初始化项。如果你打算经常这样做,可以编写一个更健壮/更少bug的函数。
第二个函数是递归的,遍历对象中的所有list,并为它们生成代码。
免责声明:这适用于具有基本数据类型的简单模型。它生成了需要清理的代码,但允许我快速继续。这里只是作为如何做到这一点的一个例子。希望它能激励人们自己去写。
在我的例子中,我有一个从数据库加载的大型数据集(results)的实例。为了从我的单元测试中移除对数据库的依赖,我把对象交给了这个函数,这个函数给出了允许我在测试类中模拟对象的代码。
private void WriteInstanciationCodeFromObject(IList results)
{
//declare the object that will eventually house C# initialization code for this class
var testMockObject = new System.Text.StringBuilder();
//start building code for this object
ConstructAndFillProperties(testMockObject, results);
var codeOutput = testMockObject.ToString();
}
private void ConstructAndFillProperties(StringBuilder testMockObject, IList results)
{
testMockObject.AppendLine("var testMock = new " + results.GetType().ToString() + "();");
foreach (object obj in results)
{
//if this object is a list, write code for its contents
if (obj.GetType().GetInterfaces().Contains(typeof(IList)))
{
ConstructAndFillProperties(testMockObject, (IList)obj);
}
testMockObject.AppendLine("testMock.Add(new " + obj.GetType().Name + "() {");
foreach (var property in obj.GetType().GetProperties())
{
//if this property is a list, write code for its contents
if (property.PropertyType.GetInterfaces().Contains(typeof(IList)))
{
ConstructAndFillProperties(testMockObject, (IList)property.GetValue(obj, null));
}
testMockObject.AppendLine(property.Name + " = (" + property.PropertyType + ")'"" + property.GetValue(obj, null) + "'",");
}
testMockObject.AppendLine("});");
}
}
这个对象可能会有一个TypeConverter,支持转换为InstanceDescriptor,这是WinForms设计器在发出c#代码生成对象时使用的。如果它不能转换为InstanceDescriptor,它将尝试使用无参数构造函数并简单地设置公共属性。InstanceDescriptor机制很方便,因为它允许您指定各种构造选项,例如带参数的构造函数,甚至是静态工厂方法调用。
我编写了一些实用程序代码,使用IL发出内存中对象的加载,基本上遵循上述模式(如果可能的话使用InstanceDescriptor,如果不使用,则简单地编写公共属性)。注意,只有当InstanceDescriptor被正确实现或者设置公共属性足以恢复对象状态时,这才会产生一个等价的对象。如果您发出IL,您也可以欺骗并直接读/写字段值(这就是DataContractSerializer所支持的),但是有许多令人讨厌的极端情况需要考虑。
我在这方面也是新手,但我也需要一个定义了层次结构的c#对象,并将其提取到对象初始化器中,以简化单元测试的设置。我从上面借了很多钱,最后得到了这个。我想改进它处理识别用户类的方式。
http://github.com/jefflomax/csharp-object-to-object-literal/blob/master/Program.csusing System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ObjectInitializer
{
public class Program
{
public enum Color { Red, Green, Blue, Yellow, Fidget } ;
public class Foo
{
public int FooId { get; set; }
public string FooName { get; set; }
}
public class Thing
{
public int ThingId { get; set; }
public string ThingName { get; set; }
public List<Foo> Foos { get; set; }
}
public class Widget
{
public long Sort { get; set; }
public char FirstLetter { get; set; }
}
public class TestMe
{
public Color Color { get; set; }
public long Key { get; set; }
public string Name { get; set; }
public DateTime Created { get; set; }
public DateTime? NCreated { get; set; }
public bool Deleted { get; set; }
public bool? NDeleted { get; set; }
public double Amount { get; set; }
public Thing MyThing { get; set; }
public List<Thing> Things { get; set; }
public List<Widget> Widgets { get; set; }
}
static void Main(string[] args)
{
var testMe = new TestMe
{
Color = Program.Color.Blue,
Key = 3,
Name = "SAK",
Created = new DateTime(2013,10,20,8,0,0),
NCreated = (DateTime?)null,
Deleted = false,
NDeleted = null,
Amount = 13.1313,
MyThing = new Thing(){ThingId=1,ThingName="Thing 1"},
Things = new List<Thing>
{
new Thing
{
ThingId=4,
ThingName="Thing 4",
Foos = new List<Foo>
{
new Foo{FooId=1, FooName="Foo 1"},
new Foo{FooId=2,FooName="Foo2"}
}
},
new Thing
{
ThingId=5,
ThingName="Thing 5",
Foos = new List<Foo>()
}
},
Widgets = new List<Widget>()
};
var objectInitializer = ToObjectInitializer(testMe);
Console.WriteLine(objectInitializer);
// This is the returned C# Object Initializer
var x = new TestMe { Color = Program.Color.Blue, Key = 3, Name = "SAK", Created = new DateTime(2013, 10, 20, 8, 0, 0), NCreated = null, Deleted = false, NDeleted = null, Amount = 13.1313, MyThing = new Thing { ThingId = 1, ThingName = "Thing 1", Foos = new List<Foo>() }, Things = new List<Thing> { new Thing { ThingId = 4, ThingName = "Thing 4", Foos = new List<Foo> { new Foo { FooId = 1, FooName = "Foo 1" }, new Foo { FooId = 2, FooName = "Foo2" } } }, new Thing { ThingId = 5, ThingName = "Thing 5", Foos = new List<Foo>() } }, Widgets = new List<Widget>() };
Console.WriteLine("");
}
public static string ToObjectInitializer(Object obj)
{
var sb = new StringBuilder(1024);
sb.Append("var x = ");
sb = WalkObject(obj, sb);
sb.Append(";");
return sb.ToString();
}
private static StringBuilder WalkObject(Object obj, StringBuilder sb)
{
var properties = obj.GetType().GetProperties();
var type = obj.GetType();
var typeName = type.Name;
sb.Append("new " + type.Name + " {");
bool appendComma = false;
DateTime workDt;
foreach (var property in properties)
{
if (appendComma) sb.Append(", ");
appendComma = true;
var pt = property.PropertyType;
var name = pt.Name;
var isList = property.PropertyType.GetInterfaces().Contains(typeof(IList));
var isClass = property.PropertyType.IsClass;
if (isList)
{
IList list = (IList)property.GetValue(obj, null);
var listTypeName = property.PropertyType.GetGenericArguments()[0].Name;
if (list != null && list.Count > 0)
{
sb.Append(property.Name + " = new List<" + listTypeName + ">{");
sb = WalkList( list, sb );
sb.Append("}");
}
else
{
sb.Append(property.Name + " = new List<" + listTypeName + ">()");
}
}
else if (property.PropertyType.IsEnum)
{
sb.AppendFormat("{0} = {1}", property.Name, property.GetValue(obj));
}
else
{
var value = property.GetValue(obj);
var isNullable = pt.IsGenericType && pt.GetGenericTypeDefinition() == typeof(Nullable<>);
if (isNullable)
{
name = pt.GetGenericArguments()[0].Name;
if (property.GetValue(obj) == null)
{
sb.AppendFormat("{0} = null", property.Name);
continue;
}
}
switch (name)
{
case "Int64":
case "Int32":
case "Int16":
case "Double":
case "Float":
sb.AppendFormat("{0} = {1}", property.Name, value);
break;
case "Boolean":
sb.AppendFormat("{0} = {1}", property.Name, Convert.ToBoolean(value) == true ? "true" : "false");
break;
case "DateTime":
workDt = Convert.ToDateTime(value);
sb.AppendFormat("{0} = new DateTime({1},{2},{3},{4},{5},{6})", property.Name, workDt.Year, workDt.Month, workDt.Day, workDt.Hour, workDt.Minute, workDt.Second);
break;
case "String":
sb.AppendFormat("{0} = '"{1}'"", property.Name, value);
break;
default:
// Handles all user classes, should likely have a better way
// to detect user class
sb.AppendFormat("{0} = ", property.Name);
WalkObject(property.GetValue(obj), sb);
break;
}
}
}
sb.Append("}");
return sb;
}
private static StringBuilder WalkList(IList list, StringBuilder sb)
{
bool appendComma = false;
foreach (object obj in list)
{
if (appendComma) sb.Append(", ");
appendComma = true;
WalkObject(obj, sb);
}
return sb;
}
}
}
它可能来得有点晚,但这是我对这个问题的五分之一。
提到的Visual Studio扩展(OmarElabd/ObjectExporter)是一个好主意,但我需要在运行时从内存对象生成c#代码,在单元测试执行期间。这是从最初的问题演变而来的:https://www.nuget.org/packages/ObjectDumper.NET/
ObjectDumper.Dump(obj, DumpStyle.CSharp);
返回变量的c#初始化代码。如果你发现问题,请告诉我,你可能想在github上报告它们。
我在寻找Matthew描述的相同类型的方法时偶然发现了这个,并受到Evan的回答的启发,编写了我自己的扩展方法。它以字符串的形式生成可编译的c#代码,可以复制/粘贴到Visual Studio中。我没有使用任何特定的格式,只是将代码输出到一行,然后使用ReSharper进行格式化。我把它用在一些大的dto上,到目前为止,它的效果很好。
下面是扩展方法和一些辅助方法:public static string ToCreationMethod(this object o)
{
return String.Format("var newObject = {0};", o.CreateObject());
}
private static StringBuilder CreateObject(this object o)
{
var builder = new StringBuilder();
builder.AppendFormat("new {0} {{ ", o.GetClassName());
foreach (var property in o.GetType().GetProperties())
{
var value = property.GetValue(o);
if (value != null)
{
builder.AppendFormat("{0} = {1}, ", property.Name, value.GetCSharpString());
}
}
builder.Append("}");
return builder;
}
private static string GetClassName(this object o)
{
var type = o.GetType();
if (type.IsGenericType)
{
var arg = type.GetGenericArguments().First().Name;
return type.Name.Replace("`1", string.Format("<{0}>", arg));
}
return type.Name;
}
GetCSharpString方法包含逻辑,并且它可以扩展到任何特定类型。它能处理字符串,整型,小数,日期只要能实现IEnumerable
private static string GetCSharpString(this object o)
{
if (o is String)
{
return string.Format("'"{0}'"", o);
}
if (o is Int32)
{
return string.Format("{0}", o);
}
if (o is Decimal)
{
return string.Format("{0}m", o);
}
if (o is DateTime)
{
return string.Format("DateTime.Parse('"{0}'")", o);
}
if (o is IEnumerable)
{
return String.Format("new {0} {{ {1}}}", o.GetClassName(), ((IEnumerable)o).GetItems());
}
return string.Format("{0}", o.CreateObject());
}
private static string GetItems(this IEnumerable items)
{
return items.Cast<object>().Aggregate(string.Empty, (current, item) => current + String.Format("{0}, ", item.GetCSharpString()));
}
我希望有人觉得这有用!
有一个类似于Evan提出的解决方案,但更适合我的特定任务。
在使用了CodeDOM和Reflection之后,我发现这对我来说太复杂了。
对象被序列化为XML,因此自然的解决方案是使用XSLT将其简单地转换为对象创建表达式。
当然,它只涵盖某些类型的情况,但可能会适用于其他人。
这是@revlucio解决方案的更新,增加了对布尔值和枚举的支持。
public static class ObjectInitializationSerializer
{
private static string GetCSharpString(object o)
{
if (o is bool)
{
return $"{o.ToString().ToLower()}";
}
if (o is string)
{
return $"'"{o}'"";
}
if (o is int)
{
return $"{o}";
}
if (o is decimal)
{
return $"{o}m";
}
if (o is DateTime)
{
return $"DateTime.Parse('"{o}'")";
}
if (o is Enum)
{
return $"{o.GetType().FullName}.{o}";
}
if (o is IEnumerable)
{
return $"new {GetClassName(o)} 'r'n{{'r'n{GetItems((IEnumerable)o)}}}";
}
return CreateObject(o).ToString();
}
private static string GetItems(IEnumerable items)
{
return items.Cast<object>().Aggregate(string.Empty, (current, item) => current + $"{GetCSharpString(item)},'r'n");
}
private static StringBuilder CreateObject(object o)
{
var builder = new StringBuilder();
builder.Append($"new {GetClassName(o)} 'r'n{{'r'n");
foreach (var property in o.GetType().GetProperties())
{
var value = property.GetValue(o);
if (value != null)
{
builder.Append($"{property.Name} = {GetCSharpString(value)},'r'n");
}
}
builder.Append("}");
return builder;
}
private static string GetClassName(object o)
{
var type = o.GetType();
if (type.IsGenericType)
{
var arg = type.GetGenericArguments().First().Name;
return type.Name.Replace("`1", $"<{arg}>");
}
return type.Name;
}
public static string Serialize(object o)
{
return $"var newObject = {CreateObject(o)};";
}
}