如何在 c# 中对具有方法调用的 LinqWhere() 进行单元测试

问题描述 投票:0回答:1

我的源代码有

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子句的存根。

我们应该如何为此编写存根?

c# linq where-clause moq xunit
1个回答
0
投票

您的问题源于

Expression<Func<T, bool>>
谓词的行为和 LINQ 的执行模型。当您在
GetSearchPredicate()
子句中使用类似
Where()
的方法时,它不会立即“运行”,而是构建一个表达式树,稍后在实际枚举数据时对其进行求值。因此,当您用
GetQueryable<Model>()
删除
LuceneProviderQueryableStub
时,它仍然需要计算表达式的
Where(GetSearchPredicate())
部分,这会调用私有方法。

为了正确进行单元测试,我们需要存根或模拟

Where
方法的行为,以便它不会评估
GetSearchPredicate()
。您可以通过几种不同的方法来解决此问题:

1。隔离逻辑: 重构方法,将数据检索逻辑与过滤逻辑分离。然后您可以独立测试每个部分。例如,测试

GetSearchPredicate()
在给定特定条件下返回正确的谓词,并测试您的数据检索方法是否在给定特定谓词的情况下正确检索数据。

2。使用模拟框架绕过Where子句:您正在使用NSubstitute,但模拟像

Where()
这样的扩展方法更具挑战性。您可以考虑使用 Moq,它支持模拟扩展方法,尽管需要一些解决方法。

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>
的模拟,它代表您的可查询数据。然后绕过
Where
方法直接返回未过滤的数据。这样,
GetSearchPredicate()
及其调用的私有方法就不会被执行。

但是,请记住这只是一种方法。单元测试的关键是隔离功能单元,因此从长远来看,重构方法可能是最可维护和最清晰的。

© www.soinside.com 2019 - 2024. All rights reserved.