动态创建任意对象以进行单元测试

时间:2018-08-14 16:40:44

标签: c# unit-testing

我们的代码库充满了包含大量数字和字符串字段的域对象。在单元测试中,我发现自己手动创建了这些不同对象的实例,如下所示:

var car1 = new Car();
car1.Make = "Make1";
car1.Model = "Model1";
car1.Year = 1;

var car2 = new Car();
car2.Make = "Make2";
car2.Model = "Model2";
car2.Year = 2;

等等。

仅通过一个函数调用自动生成任何类型的对象的最干净方法是什么?

请记住,我不希望使用随机值生成字段。我想要的是任意的可重复值(如上面的1和2)。

2 个答案:

答案 0 :(得分:0)

我最终选择了一种通用方法,该方法使用反射来自动完成问题,并手动完成。用这种方法创建的任何字段都可以被覆盖,任何复杂的类型都必须手动设置。布尔值设置为true。我很好奇,听到人们对这样做的想法。在构建单元测试时,这为我节省了大量时间和精力。

这就是我的测试最终的外观(一个简单的例子,没有复杂的类型或我想用特定值覆盖的字符串/数字字段,尽管这些也可以处理):

[Test]
public void TestCreateCars()
{
    // Arrange
    var expectedCars = new List<Car>();
    expectedCars.Add(TestUtils.CreateObject<Car>(1));
    expectedCars.Add(TestUtils.CreateObject<Car>(2));
    expectedCars.Add(TestUtils.CreateObject<Car>(3));

    // using moq but anything could be used
    _carService.Setup(x => x.GetNewCarsInfo()).Returns(cars);

    var carsFactory = new carFactory(_carService);

    // Act
    var cars = carsFactory.CreateCars();

    // Assert
    Assert.AreEqual(3, cars.Count);
    TestUtils.AssertObjectDefaultFields(cars[0], 1);
    TestUtils.AssertObjectDefaultFields(cars[1], 2);
    TestUtils.AssertObjectDefaultFields(cars[2], 3);

    _carService.VerifyAll();
}

创建对象的方法:

// Use this to instantiate objects of types with a large number of int/string fields for testing.  You can override any values after calling this - complex fields won't be set
//  -sets numeric values on that object to its 'id'
//  -sets string values on that object to the name of the field concatenated with the 'id'
public static T CreateObject<T>(int id)
{
    var instance = (T)Activator.CreateInstance(typeof(T));

    // only set properties with a visible setter
    var properties = typeof(T).GetProperties().Where(prop => prop.GetSetMethod() != null);

    foreach (var property in properties)
    {
        var type = property.PropertyType;
        if (IsNumeric(type))
        {
            type = Nullable.GetUnderlyingType(type) ?? type;
            var value = Convert.ChangeType(id, type);

            property.SetValue(instance, value);
        }
        else if (property.PropertyType == typeof(string))
        {
            property.SetValue(instance, property.Name + id);
        }
        else if (property.PropertyType == typeof(bool))
        {
            property.SetValue(instance, true);
        }
    }

    return instance;
}

断言以这种方式创建的对象的方法具有预期值:

// Use this to assert that an object created by CreateObject<T> has all of the 'default' values:
//  -numeric values equal its 'id'
//  -string values equal the name of the field concatenated with the 'id'
// 
// unsetProperties: property names that we want to assert are their default values
// ignoreProperties: property names that we don't want to assert anything against - we should assert against these outside of this method
public static void AssertObjectDefaultFields<T>(T obj, int id, HashSet<string> unsetProperties = null, HashSet<string> ignoreProperties = null)
{
    // only test properties with a visible setter, otherwise it wouldnt have been set
    var properties = typeof(T).GetProperties().Where(prop => prop.GetSetMethod() != null);
    unsetProperties = unsetProperties ?? new HashSet<String>();
    ignoreProperties = ignoreProperties ?? new HashSet<String>();

    foreach (var property in properties)
    {
        if(!ignoreProperties.Contains(property.Name))
        {
            if (unsetProperties.Contains(property.Name))
            {
                var defaultValue = property.PropertyType.IsValueType ? Activator.CreateInstance(property.PropertyType) : null;
                Assert.AreEqual(defaultValue, property.GetValue(obj));
            }
            else if (IsNumeric(property.PropertyType))
            {
                Assert.AreEqual(id, property.GetValue(obj));
            }
            else if (property.PropertyType == typeof(string))
            {
                Assert.AreEqual(property.Name + id, property.GetValue(obj));
            }
            else if (property.PropertyType == typeof(bool))
            {
                Assert.AreEqual(true, property.GetValue(obj));
            }
        }
    }
}

用于测试字段的类型是否为数字:

private static bool IsNumeric(Type type)
{
    var NumericTypes = new HashSet<Type>
    {
        typeof(int),  typeof(double),  typeof(decimal),
        typeof(long), typeof(short),   typeof(sbyte),
        typeof(byte), typeof(ulong),   typeof(ushort),
        typeof(uint), typeof(float)
    };

    return NumericTypes.Contains(Nullable.GetUnderlyingType(type) ?? type);
}

答案 1 :(得分:0)

由于它是域对象,为什么不使用工厂?

无论您设置了什么值,单元测试都应该有效。

如果要测试特定值,则应编写特定测试。

或者您的需求不同吗?