如何在C#中单元测试具有方法调用的Linq Where()

xtfmy6hx  于 2023-09-28  发布在  C#
关注(0)|答案(1)|浏览(125)

我的源代码

var results = searchContext.GetQueryable<Model>().Where(GetSearchPredicate()).GetResults();
private Expression<Func<Model, bool>> GetSearchPredicate()
{
    Expression<Func<Model, bool>> filterSearch = x => true;
    filterSearch = filterSearch.And(GetTemplateFilterPredicate());
    return filterSearch;
}

这里GetTemplateFilterPredicate是另一个私有方法,它执行更多的过滤。
我的单元测试逻辑

[Theory]
[AutoData]
    public void GetEvent_ReturnResult()
    {
        var Obj = new List<Model>
        {
            new Model
                {
                    Name="testname123",
                    TemplateName="Analysis",
                    Id = "123",
                    Description="TestDescription123",
                }
        }.AsEnumerable();
        var searchContext = Substitute.For<IProviderSearchContext>();
        var queryable = new LuceneProviderQueryableStub<Model>(Obj);
        searchContext.GetQueryable<Model>().Returns(queryable);

        var mockRepo = new Mock<AnalysisService>();
        mockRepo.Protected()
            .Setup<IProviderSearchContext>("GetSearchContext")
            .Returns(searchContext);

        // Act
        var result = mockRepo.Object.Get("123");
        //Assert
    }

我的LuceneProviderQueryableStub

public class LuceneProviderQueryableStub<TElement> : IOrderedQueryable<TElement>, IOrderedQueryable, IQueryProvider, IQueryable<TElement>
{
    private readonly EnumerableQuery<TElement> innerQueryable;

    public Type ElementType { get { return ((IQueryable)innerQueryable).ElementType; } }

    public Expression Expression { get { return ((IQueryable)innerQueryable).Expression; } }

    public IQueryProvider Provider { get { return this; } }

    public LuceneProviderQueryableStub(IEnumerable<TElement> enumerable)
    {
        innerQueryable = new EnumerableQuery<TElement>(enumerable);
    }
    public LuceneProviderQueryableStub(Expression expression)
    {
        innerQueryable = new EnumerableQuery<TElement>(expression);
    }
    public IQueryable CreateQuery(Expression expression)
    {
        expression = new FilterCallsReplacer().Visit(expression);
        return new LuceneProviderQueryableStub<TElement>((IEnumerable<TElement>)((IQueryProvider)innerQueryable).CreateQuery(expression));
    }
    public IQueryable<TElement1> CreateQuery<TElement1>(Expression expression)
    {
        expression = new FilterCallsReplacer().Visit(expression);
        return (IQueryable<TElement1>)new LuceneProviderQueryableStub<TElement>((IEnumerable<TElement>)((IQueryProvider)innerQueryable).CreateQuery(expression));
    }
    public object Execute(Expression expression)
    {
        throw new NotImplementedException();
    }
    public TResult Execute<TResult>(Expression expression)
    {
        var items = this.ToArray();
        object results = new SearchResults<TElement>(items.Select(s => new SearchHit<TElement>(0, s)), items.Length);
        return (TResult)results;
    }
    public IEnumerator<TElement> GetEnumerator()
    {
        return ((IEnumerable<TElement>)innerQueryable).GetEnumerator();
    }
    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}
internal class FilterCallsReplacer : ExpressionVisitor
{
    private static readonly MethodInfo FilterMethod = typeof(QueryableExtensions)
        .GetMethod(nameof(QueryableExtensions.Filter), BindingFlags.Static | BindingFlags.Public);

    private static readonly MethodInfo WhereMethod = typeof(Queryable).GetMethods(BindingFlags.Static | BindingFlags.Public)
        .First(method => method.Name == nameof(Queryable.Where));

    protected override Expression VisitMethodCall(MethodCallExpression node)
    {
        return IsFilterMethod(node)
            ? RewriteToWhere(node)
            : base.VisitMethodCall(node);
    }
    private static bool IsFilterMethod(MethodCallExpression node)
    {
        return node.Method.IsGenericMethod && node.Method.GetGenericMethodDefinition() == FilterMethod;
    }
    private Expression RewriteToWhere(MethodCallExpression node)
    {
        var arguments = node.Arguments.ToArray();
        var type = node.Method.GetGenericArguments().First();
        var whereMethod = WhereMethod.MakeGenericMethod(type);

        return Expression.Call(whereMethod, arguments);
    }
}

我的期望是绕过查询并返回Mock数据。但是调试器在Where查询中命中了私有方法,这是不应该发生的。
我是单元测试的新手,不确定Where子句的存根有私有方法调用。
我们应该怎么写存根呢?

uqjltbpv

uqjltbpv1#

您的问题源于Expression<Func<T, bool>> predicate 的行为和LINQ的执行模型。当您在Where()子句中使用类似GetSearchPredicate()的方法时,它不会立即“运行”,而是构建一个表达式树,稍后在实际枚举数据时对其进行计算。因此,当您使用LuceneProviderQueryableStub剔除GetQueryable<Model>()时,它仍然需要计算表达式的Where(GetSearchPredicate())部分,这将调用私有方法。
为了正确地进行单元测试,我们需要存根或模拟Where方法的行为,使其不计算GetSearchPredicate()。有几种不同的方法可以实现这一点:

**1.隔离逻辑:**重构方法,将取数逻辑和过滤逻辑分离。然后,您可以独立测试每个部分。例如,测试GetSearchPredicate()在给定的特定条件下返回正确的 predicate ,并测试您的数据检索方法在给定的特定 predicate 下正确地检索数据。
**2.使用Mocking Framework绕过Where子句:**您使用的是NSubstitute,但模拟Where()等扩展方法更具挑战性。您可以考虑使用Moq,它支持mocking扩展方法,尽管有一些变通办法。
**3.截断整个Where方法:**您可以更深入地使用截断并替换整个Where方法,但这很复杂,并且使测试不那么直观。

让我们用Moq尝试第二种方法:

csharp

[Theory]
[AutoData]
public void GetEvent_ReturnResult()
{
    var Obj = new List<Model>
    {
        new Model
        {
            Name="testname123",
            TemplateName="Analysis",
            Id = "123",
            Description="TestDescription123",
        }
    }.AsQueryable();

    var mockSet = new Mock<DbSet<Model>>();
    mockSet.As<IQueryable<Model>>().Setup(m => m.Provider).Returns(Obj.Provider);
    mockSet.As<IQueryable<Model>>().Setup(m => m.Expression).Returns(Obj.Expression);
    mockSet.As<IQueryable<Model>>().Setup(m => m.ElementType).Returns(Obj.ElementType);
    mockSet.As<IQueryable<Model>>().Setup(m => m.GetEnumerator()).Returns(Obj.GetEnumerator());

    mockSet.Setup(m => m.Where(It.IsAny<Expression<Func<Model, bool>>>())).Returns(Obj); // Bypass the Where

    var mockContext = new Mock<YourDbContext>(); // Replace with your actual DbContext type
    mockContext.Setup(c => c.Set<Model>()).Returns(mockSet.Object);

    // ... Rest of the setup for your test

    var result = mockRepo.Object.Get("123");

    //Assert
}

这个设置使用Moq创建一个DbSet<Model>的mock,它表示您的可查询数据。然后绕过Where方法直接返回未过滤的数据。这样,GetSearchPredicate()和它调用的私有方法就不会被执行。
但是,请记住,这只是一种方法。单元测试的关键是隔离功能单元,因此从长远来看,重构方法可能是最主要和最清晰的。

相关问题