You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为使用EF Core及仓储模式的带分页功能的控制器异步方法编写单元测试

如何为使用EF Core及仓储模式的带分页功能的控制器异步方法编写单元测试

嘿,我来帮你搞定这个单元测试的问题!针对你这种用EF Core+仓储模式、带分页的控制器异步方法,咱们一步步拆解,写出靠谱的测试代码:

先明确测试目标

咱们的核心是隔离控制器的依赖(仓储和数据库),只验证控制器的分页逻辑是否正确:比如有没有正确计算分页偏移量、返回的分页数据总数/当前页数据是否符合预期,异步方法是否能正常执行。

方案一:Mock 暴露IQueryable的仓储(适配你现有代码结构)

你现在的仓储是直接暴露IQueryable<Product>的,这种情况下要支持异步查询(比如CountAsync、ToListAsync),需要给Mock的IQueryable加个异步支持的包装,咱们一步步来:

1. 先补全控制器的异步分页方法示例

假设你的控制器异步分页方法是这样的(毕竟你贴的代码没写完,我补个最常见的场景):

using Microsoft.AspNetCore.Mvc;
using Project.Models;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;
using System.Linq;

namespace Project.Controllers
{
    public class ProductsController : Controller
    {
        private readonly IProductRepository _repo;

        public ProductsController(IProductRepository repo)
        {
            _repo = repo;
        }

        // 带分页的异步方法
        public async Task<IActionResult> Index(int page = 1, int pageSize = 10)
        {
            var totalItems = await _repo.Products.CountAsync();
            var pagedProducts = await _repo.Products
                .Skip((page - 1) * pageSize)
                .Take(pageSize)
                .ToListAsync();

            // 假设你有对应的视图模型类
            var vm = new ProductListViewModel
            {
                Products = pagedProducts,
                PagingInfo = new PagingInfo
                {
                    CurrentPage = page,
                    ItemsPerPage = pageSize,
                    TotalItems = totalItems
                }
            };

            return View(vm);
        }
    }

    // 配套视图模型
    public class ProductListViewModel
    {
        public List<Product> Products { get; set; }
        public PagingInfo PagingInfo { get; set; }
    }

    public class PagingInfo
    {
        public int CurrentPage { get; set; }
        public int ItemsPerPage { get; set; }
        public int TotalItems { get; set; }
    }
}

2. 编写异步IQueryable的辅助类

因为EF Core的异步扩展方法需要IQueryable实现IAsyncQueryProvider接口,所以咱们写个辅助类来包装普通的IQueryable,让它支持异步:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;

public class AsyncQueryProvider<TEntity> : IAsyncQueryProvider
{
    private readonly IQueryProvider _innerProvider;

    public AsyncQueryProvider(IQueryProvider innerProvider)
    {
        _innerProvider = innerProvider;
    }

    public IQueryable CreateQuery(Expression expression)
        => new AsyncEnumerable<TEntity>(expression);

    public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
        => new AsyncEnumerable<TElement>(expression);

    public object Execute(Expression expression)
        => _innerProvider.Execute(expression);

    public TResult Execute<TResult>(Expression expression)
        => _innerProvider.Execute<TResult>(expression);

    public TResult ExecuteAsync<TResult>(Expression expression, CancellationToken cancellationToken)
    {
        var resultType = typeof(TResult).GetGenericArguments()[0];
        var syncExecute = _innerProvider.Execute(expression);
        return (TResult)Task.FromResult(syncExecute).GetType().GetMethod("Result").Invoke(Task.FromResult(syncExecute), null);
    }
}

public class AsyncEnumerable<T> : EnumerableQuery<T>, IAsyncEnumerable<T>, IQueryable<T>
{
    public AsyncEnumerable(IEnumerable<T> enumerable) : base(enumerable) { }
    public AsyncEnumerable(Expression expression) : base(expression) { }

    public IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = default)
        => new AsyncEnumerator<T>(this.AsEnumerable().GetEnumerator());

    IQueryProvider IQueryable.Provider => new AsyncQueryProvider<T>(this.Provider);
}

public class AsyncEnumerator<T> : IAsyncEnumerator<T>
{
    private readonly IEnumerator<T> _innerEnumerator;

    public AsyncEnumerator(IEnumerator<T> innerEnumerator)
    {
        _innerEnumerator = innerEnumerator;
    }

    public ValueTask DisposeAsync()
    {
        _innerEnumerator.Dispose();
        return ValueTask.CompletedTask;
    }

    public ValueTask<bool> MoveNextAsync()
        => ValueTask.FromResult(_innerEnumerator.MoveNext());

    public T Current => _innerEnumerator.Current;
}

3. 编写单元测试代码

用xUnit+Moq来写测试(这俩是.NET里最常用的测试框架):

using Xunit;
using Moq;
using Project.Controllers;
using Project.Models;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Project.Tests.Controllers
{
    public class ProductsControllerTests
    {
        [Fact]
        public async Task Index_ReturnsCorrectPagedData_ForSecondPage()
        {
            // 1. 准备测试数据
            var testProducts = new List<Product>
            {
                new Product { Id = 1, Name = "商品1" },
                new Product { Id = 2, Name = "商品2" },
                new Product { Id = 3, Name = "商品3" },
                new Product { Id = 4, Name = "商品4" },
                new Product { Id = 5, Name = "商品5" }
            }.AsQueryable();

            // 2. 包装成支持异步的IQueryable
            var asyncTestProducts = new AsyncEnumerable<Product>(testProducts);

            // 3. Mock仓储
            var mockRepo = new Mock<IProductRepository>();
            mockRepo.Setup(repo => repo.Products).Returns(asyncTestProducts);

            // 4. 创建控制器实例
            var controller = new ProductsController(mockRepo.Object);

            // 5. 调用要测试的异步方法(测试第2页,每页2条)
            var result = await controller.Index(page: 2, pageSize: 2);

            // 6. 断言验证结果
            var viewResult = Assert.IsType<ViewResult>(result);
            var vm = Assert.IsType<ProductListViewModel>(viewResult.Model);
            
            // 验证分页数据:第2页应该返回商品4、商品5
            Assert.Equal(2, vm.Products.Count);
            Assert.Contains(vm.Products, p => p.Id == 4);
            Assert.Contains(vm.Products, p => p.Id == 5);
            // 验证分页信息
            Assert.Equal(5, vm.PagingInfo.TotalItems);
            Assert.Equal(2, vm.PagingInfo.CurrentPage);
            Assert.Equal(2, vm.PagingInfo.ItemsPerPage);
        }
    }
}

方案二:优化仓储接口,让测试更简单

如果不想写上面的辅助类,你可以把分页和异步逻辑封装到仓储里,让控制器只调用仓储的封装方法,这样Mock起来会轻松很多:

1. 修改仓储接口与实现

// 修改后的仓储接口
namespace Project.Models;
public interface IProductRepository
{
    // 封装分页和异步查询
    Task<int> GetTotalProductCountAsync();
    Task<List<Product>> GetPagedProductsAsync(int page, int pageSize);
}

// 实现类更新
public class EFProductRepository : IProductRepository
{
    private readonly ProjectDBContext _context;

    public EFProductRepository(ProjectDBContext ctx) => _context = ctx;

    public async Task<int> GetTotalProductCountAsync()
        => await _context.Products.CountAsync();

    public async Task<List<Product>> GetPagedProductsAsync(int page, int pageSize)
        => await _context.Products
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .ToListAsync();
}

2. 同步更新控制器方法

public async Task<IActionResult> Index(int page = 1, int pageSize = 10)
{
    var totalItems = await _repo.GetTotalProductCountAsync();
    var pagedProducts = await _repo.GetPagedProductsAsync(page, pageSize);

    var vm = new ProductListViewModel
    {
        Products = pagedProducts,
        PagingInfo = new PagingInfo
        {
            CurrentPage = page,
            ItemsPerPage = pageSize,
            TotalItems = totalItems
        }
    };

    return View(vm);
}

3. 编写简洁的单元测试

这种情况下Mock仓储就不用管IQueryable的异步细节了,直接Mock返回结果就行:

[Fact]
public async Task Index_ReturnsCorrectPagedData_ForSecondPage()
{
    // 1. Mock仓储
    var mockRepo = new Mock<IProductRepository>();
    mockRepo.Setup(r => r.GetTotalProductCountAsync()).ReturnsAsync(5);
    mockRepo.Setup(r => r.GetPagedProductsAsync(2, 2))
        .ReturnsAsync(new List<Product>
        {
            new Product { Id = 4, Name = "商品4" },
            new Product { Id = 5, Name = "商品5" }
        });

    // 2. 创建控制器
    var controller = new ProductsController(mockRepo.Object);

    // 3. 调用方法
    var result = await controller.Index(2, 2);

    // 4. 断言验证
    var viewResult = Assert.IsType<ViewResult>(result);
    var vm = Assert.IsType<ProductListViewModel>(viewResult.Model);
    
    Assert.Equal(2, vm.Products.Count);
    Assert.Equal(5, vm.PagingInfo.TotalItems);
    Assert.Equal(2, vm.PagingInfo.CurrentPage);
}

一些小提示

  • 单元测试要聚焦控制器的逻辑:比如验证分页参数是否正确传递、视图模型是否正确组装,不用关心仓储和数据库的具体实现。
  • 如果你用的是xUnit,测试方法必须加async,返回Task,这样才能正确执行异步测试。
  • 方案二的代码结构更符合“单一职责”:仓储负责数据查询逻辑,控制器只负责接收请求、调用仓储、返回结果,测试起来也更清晰。

备注:内容来源于stack exchange,提问作者mort89

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.16 08:18:10