c#模拟EF6上下文并处理自动增量主键

时间:2016-11-30 00:32:51

标签: c# entity-framework unit-testing mocking entity-framework-6

有没有人找到一种在模拟上下文中自动增加主键的好方法,以便在测试服务层时起作用?

在大多数情况下,可以将主键作为要测试的数据的一部分进行播种。但是许多服务层方法处理创建多个对象或将其他进程链接在一起,如果您不负责传递所有已创建的数据,则很快就会失败。我想到可能会向SaveChangesAsync()添加一个Callback()来查看数据创建,并以增量方式自动生成主键,但实现起来并不简单。

var organization = new PrivateOrganization();
organization.Name = "New Test Organization";
organization.Description = "New Test Organization description";
organization.OrganizationTypeId = ITNOrganizationTypes.Agency;
organization.OrganizationStatusTypeId = (int)ITNOrganizationStatusTypes.Enabled;
organization.ShortCode = "Test";

var newOrg = await _service.InsertPrivateOrganizationAsync(organization);

_mockPrivateOrganizationsSet.Verify(m => m.Add(It.IsAny<PrivateOrganization>()), Times.Once());
MockTenantContext.Verify(m => m.SaveChangesAsync(), Times.Once());

// validation passes, but contains no auto-generated primary key.

1 个答案:

答案 0 :(得分:2)

我必须为此开发自己的解决方案如下:

/// <summary>
/// A helper class for managing custom behaviors of Mockable database contexts
/// </summary>
public static partial class EFSaveChangesBehaviors
{
    /// <summary>
    /// Enable auto-incrementing of primary key values upon SaveChanges/SaveChangesAsync
    /// </summary>
    /// <typeparam name="T">The type of context to enable auto-incrementing on</typeparam>
    /// <param name="context">The context to enable this feature</param>
    public static void EnableAutoIncrementOnSave<T>(this Mock<T> context) where T : DbContext
    {
        context.Setup(m => m.SaveChangesAsync())
            .Callback(() =>
            {
                EFSaveChangesBehaviors.SaveChangesIncrementKey(context.Object);
            })
            .Returns(() => Task.Run(() => { return 1; }))
            .Verifiable();

        context.Setup(m => m.SaveChanges())
            .Callback(() =>
            {
                EFSaveChangesBehaviors.SaveChangesIncrementKey(context.Object);
            })
            .Returns(() => { return 1; })
            .Verifiable();
    }

    /// <summary>
    /// Implements key incrementing of data records that are pending to be added to the context
    /// </summary>
    /// <param name="context"></param>
    public static void SaveChangesIncrementKey(DbContext context)
    {
        var tablesWithNewData = GetUnsavedRows<DbContext>(context);
        for (int i = 0; i < tablesWithNewData.Count; i++)
        {
            long nextPrimaryKeyValue = 0;
            var tableWithDataProperty = tablesWithNewData[i];
            var tableWithDataObject = tableWithDataProperty.GetValue(context);
            if (tableWithDataObject != null)
            {
                var tableWithDataQueryable = tableWithDataObject as IQueryable<object>;

                // 1) get the highest value in the DbSet<> (table) to continue auto-increment from
                nextPrimaryKeyValue = IterateAndPerformAction(context, tableWithDataQueryable, tableWithDataProperty, nextPrimaryKeyValue, (primaryExistingKeyValue, primaryKeyRowObject, primaryKeyProperty) =>
                {
                    if (primaryExistingKeyValue > nextPrimaryKeyValue)
                        nextPrimaryKeyValue = Convert.ToInt64(primaryExistingKeyValue);
                    return nextPrimaryKeyValue;
                });

                // 2) increase the value of the record's primary key on each iteration
                IterateAndPerformAction(context, tableWithDataQueryable, tableWithDataProperty, nextPrimaryKeyValue, (primaryKeyExistingValue, primaryKeyRowObject, primaryKeyProperty) =>
                {
                    if (primaryKeyExistingValue == 0)
                    {
                        nextPrimaryKeyValue++;
                        Type propertyType = primaryKeyProperty.PropertyType;
                        if (propertyType == typeof(Int64))
                            primaryKeyProperty.SetValue(primaryKeyRowObject, nextPrimaryKeyValue);
                        else if (propertyType == typeof(Int32))
                            primaryKeyProperty.SetValue(primaryKeyRowObject, Convert.ToInt32(nextPrimaryKeyValue));
                        else if (propertyType == typeof(Int16))
                            primaryKeyProperty.SetValue(primaryKeyRowObject, Convert.ToInt16(nextPrimaryKeyValue));
                        else if (propertyType == typeof(byte))
                            primaryKeyProperty.SetValue(primaryKeyRowObject, Convert.ToByte(nextPrimaryKeyValue));
                        else
                            throw new System.NotImplementedException($"Cannot manage primary keys of type: {propertyType.FullName}");
                    }
                    return nextPrimaryKeyValue;
                });
            }
        }
    }

    /// <summary>
    /// Get a list of properties for a data table that are indicated as a primary key
    /// </summary>
    /// <param name="t"></param>
    /// <param name="context"></param>
    /// <returns></returns>
    /// <remarks>Reflection must be used, as the ObjectContext is not mockable</remarks>
    public static PropertyInfo[] GetPrimaryKeyNamesUsingReflection(Type t, DbContext context)
    {
        var properties = t.GetProperties();
        var keyNames = properties
            .Where(prop => Attribute.IsDefined(prop, typeof(System.ComponentModel.DataAnnotations.KeyAttribute)))
            .ToArray();

        return keyNames;
    }

    /// <summary>
    /// Iterates a table's data and allows an action to be performed on each row
    /// </summary>
    /// <param name="context">The database context</param>
    /// <param name="tableWithDataQueryable"></param>
    /// <param name="tableWithDataProperty"></param>
    /// <param name="nextPrimaryKeyValue"></param>
    /// <param name="action"></param>
    /// <returns></returns>
    private static long IterateAndPerformAction(DbContext context, IQueryable<object> tableWithDataQueryable, PropertyInfo tableWithDataProperty, long nextPrimaryKeyValue, Func<long, object, PropertyInfo, long> action)
    {
        foreach (var primaryKeyRowObject in tableWithDataQueryable)
        {
            // create a primary key for the object
            if (tableWithDataProperty.PropertyType.GenericTypeArguments.Length > 0)
            {
                var dbSetType = tableWithDataProperty.PropertyType.GenericTypeArguments[0];
                // find the primary key property
                var primaryKeyProperty = GetPrimaryKeyNamesUsingReflection(dbSetType, context).FirstOrDefault();
                if (primaryKeyProperty != null)
                {
                    var primaryKeyValue = primaryKeyProperty.GetValue(primaryKeyRowObject) ?? 0L;
                    nextPrimaryKeyValue = action(Convert.ToInt64(primaryKeyValue), primaryKeyRowObject, primaryKeyProperty);
                }
            }
        }
        return nextPrimaryKeyValue;
    }

    /// <summary>
    /// Get a list of objects which are pending to be added to the context
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="context"></param>
    /// <returns></returns>
    private static IList<PropertyInfo> GetUnsavedRows<T>(T context)
    {
        // get list of properties of type DbSet<>
        var dbSetProperties = new List<PropertyInfo>();
        var properties = context.GetType().GetProperties();
        foreach (var property in properties)
        {
            var setType = property.PropertyType;
            var isDbSet = setType.IsGenericType && (typeof(IDbSet<>).IsAssignableFrom(setType.GetGenericTypeDefinition()) || setType.GetInterface(typeof(IDbSet<>).FullName) != null);
            if (isDbSet)
            {
                dbSetProperties.Add(property);
            }
        }

        return dbSetProperties;
    }


}

用法:

// enable auto-increment in our in-memory database
MockTenantContext.EnableAutoIncrementOnSave();