实体框架核心模拟.Include和.Theninclude with nsubstitute

时间:2017-08-26 18:48:24

标签: c# unit-testing asp.net-core entity-framework-core nsubstitute

我确实使用nsubstitue为Entity Famework Core 1.1.2创建了DbSet的模拟

class FakeDbSet<TEntity> : DbSet<TEntity>, IQueryable<TEntity>, IAsyncEnumerable<TEntity> where TEntity : class

它有一个内部列表,用于保存数据以模拟添加,查找和删除方法。如何模拟.Include和.ThenInclude方法来使连接工作?

我目前的FakeDbSet实现:

/// <summary>
///     FakeDbSet holds entries in interal list to fake add and delete methods
///     Mocking DbSet normaly would only work for getter not for setter
/// </summary>
/// <typeparam name="TEntity"></typeparam>
class FakeDbSet<TEntity> : DbSet<TEntity>, IQueryable<TEntity>, IAsyncEnumerable<TEntity> where TEntity : class
{
    /// <summary>
    /// Static constructor. Determines the which properties are key properties
    /// </summary>
    static FakeDbSet()
    {
        var type = typeof(TEntity);

        foreach (var property in type
            .GetProperties()
            .Where(v => v.GetCustomAttributes(false).OfType<KeyAttribute>().Any()))
        {
            Keys.Add(property);
        }
    }

    /// <summary>
    /// Contains PropertyInfo objects for each of the key properties
    /// </summary>
    private static readonly List<PropertyInfo> Keys = new List<PropertyInfo>();

    /// <summary>
    /// The data we will query against in a List object
    /// </summary>
    private readonly IList<TEntity> _data;

    /// <summary>
    /// The data we will query against in a IQueryable object
    /// </summary>
    private readonly IQueryable<TEntity> _queryable;

    /// <summary>
    /// A dictionary to look up the current status of an object
    /// </summary>
    private readonly Dictionary<TEntity, EntityStatus> _entityStatus =
        new Dictionary<TEntity, EntityStatus>();

    /// <summary>
    /// Observable collection of data
    /// </summary>

    /// <summary>
    /// Constructor.  Expects an IList of entity type
    /// that becomes the data store
    /// </summary>
    /// <param name="data"></param>
    public FakeDbSet(IList<TEntity> data)
    {
        _data = data;
        _entityStatus.Clear();
        foreach (var item in data)
        {
            _entityStatus[item] = EntityStatus.Normal;
        }
        _queryable = data.AsQueryable();

        // The fake provider wraps the real provider (for "List<TEntity")
        // so that it can log activities
        Provider = new FakeAsyncQueryProvider<TEntity>(_queryable.Provider);
    }


    /// <inheritdoc />
    public override EntityEntry<TEntity> Add(TEntity entity)
    {
        _data.Add(entity);
        _entityStatus[entity] = EntityStatus.Added;

        return null;
    }

    /// <inheritdoc />
    public override async Task<EntityEntry<TEntity>> AddAsync(TEntity entity, CancellationToken cancellationToken = new CancellationToken())
    {
        return await Task.FromResult(Add(entity));
    }

    /// <inheritdoc />
    public override Task AddRangeAsync(params TEntity[] entities)
    {
        throw new NotImplementedException();
    }

    /// <summary>
    /// Implements the Find function of IdbSet.
    /// Depends on the keys collection being
    /// set to the key types of this entity
    /// </summary>
    /// <param name="keyValues"></param>
    /// <returns></returns>
    public override TEntity Find(params object[] keyValues)
    {
        if (keyValues.Length != Keys.Count)
        {
            throw new ArgumentException(
                string.Format("Must supply {0} key values", Keys.Count),
                "keyValues"
            );
        }

        var query = _queryable;

        var parameterExpression = Expression.Parameter(typeof(TEntity), "v");

        for (int i = 0; i < Keys.Count; i++)
        {
            var equalsExpression = Expression.Equal(
                // key property
                Expression.Property(parameterExpression, Keys[i]),
                // key value
                Expression.Constant(keyValues[i], Keys[i].PropertyType)
            );

            var whereClause = (Expression<Func<TEntity, bool>>) Expression.Lambda(
                equalsExpression,
                new ParameterExpression[] {parameterExpression}
            );

            query = query.Where(whereClause);
        }

        var result = query.ToList();

        return result.SingleOrDefault();
    }

    public override async Task<TEntity> FindAsync(params object[] keyValues)
    {
        return await new Task<TEntity>(() => Find(keyValues));
    }

    /// <summary>
    /// Implements the Remove function of IDbSet
    /// </summary>
    /// <param name="entity"></param>
    /// <returns></returns>
    public override EntityEntry<TEntity> Remove(TEntity entity)
    {
        _data.Remove(entity);
        _entityStatus[entity] = EntityStatus.Deleted;

        return null;
    }

    public IEnumerator<TEntity> GetEnumerator()
    {
        return _queryable.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return _queryable.GetEnumerator();
    }

    public Type ElementType => _queryable.ElementType;

    public Expression Expression => _queryable.Expression;

    public IQueryProvider Provider { get; }

    public enum EntityStatus
    {
        None,
        Added,
        Deleted,
        Normal
    }

    /// <inheritdoc />
    IAsyncEnumerator<TEntity> IAsyncEnumerable<TEntity>.GetEnumerator()
    {
        return new FakeAsyncEnumerator<TEntity>(_queryable.GetEnumerator());
    }
}

1 个答案:

答案 0 :(得分:0)

有一种方法可以在.NET Core中执行此操作。我意识到你的问题有点陈旧,我希望你已经找到了答案,但以防万一,这是我采用的策略。一般的想法是创建自己的同名扩展方法,它们覆盖EFCore扩展方法并使用可配置的功能(通过公开的静态属性)。

QueryableExtensions.cs - 您的重写扩展方法。

public static class QueryableExtensions
{
    public static IIncluder Includer = null;
    public static IIncludableQueryable<T, TProperty> Include<T, TProperty>(
        this IQueryable<T> source,
        Expression<Func<T, TProperty>> path
    )
        where T : class
    {
        return Includer.Include(source, path);
    }
}

DbIncluder.cs - 默认功能的包装。

using EFCore = Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions;

public class DbIncluder : IIncluder
{
    public IIncludableQueryable<T, TProperty> Include<T, TProperty>(
        IQueryable<T> source,
        Expression<Func<T, TProperty>> path
    )
        where T : class
    {
        return EFCore.Include(source, path);
    }
}

IIncluder.cs

public interface IIncluder
{
    IIncludableQueryable<T, TProperty> Include<T, TProperty>(
        IQueryable<T> source,
        Expression<Func<T, TProperty>> path
    ) where T : class;
}

然后,在您的Repository.cs(例如)中,您可以替换Mock IIncluder:

public class Repository : IRepository
{
    static Repository()
    {
        QueryableExtensions.Includer
            = QueryableExtensions.Includer ?? new DbIncluder();
    }


    // ...
}

只要您在使用Repo之前设置了QueryableExtensions.Includer = [您的模拟],它就应该使用模拟。请注意,此模式也可用于任何其他EntityFrameworkCore扩展方法。

这是我在以下网站找到的解决方案的修改版本:

http://blogs.clariusconsulting.net/kzu/how-to-design-a-unit-testable-domain-model-with-entity-framework-code-first/