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

PnP.Core中IFolderCollection模拟失败:SharePoint文档库Mock异常排查

问题:SharePoint文档库Mock模拟时文件夹集合为空导致空引用错误

我需要模拟一个返回6个文件的SharePoint文档库,为此编写了SharePointDocLibraryUtilsTestFactory类构建Mock系统,但调试时发现mockFolderCollection为空,调用被测方法ProcessFolderAsync时传入的folder参数为null,引发空引用错误。


1. 测试工厂代码

public class SharePointDocLibraryUtilsTestFactory
{
    private Mock<IPnPCoreSDKClient> _mockPnPCoreSDKClient;
    private Mock<IPnPContext> _mockPnpContext;
    private ITestOutputHelper _output;

    public SharePointDocLibraryUtilsTestFactory(ITestOutputHelper output)
    {
        _mockPnPCoreSDKClient = new Mock<IPnPCoreSDKClient>();
        _mockPnpContext = new Mock<IPnPContext>();
        _mockPnPCoreSDKClient.Setup(client => client.GetPnPContextAsync(It.IsAny<string>())).ReturnsAsync(_mockPnpContext.Object);
        _output = output;
    }

    public IList SetupMockSharePointLibrary()
    {
        var mockList = new Mock<IList>();
        var mockRootFolder = new Mock<IFolder>();
        var mockWeb = new Mock<IWeb>();
        var subfolders = new List<IFolder>();

        for (int i = 0; i < 3; i++)
        {
            var mockFolder = new Mock<IFolder>();
            var mockFileCollection = new Mock<IFileCollection>();
            var mockFiles = new List<IFile>();

            for (int j = 0; j < 2; j++)
            {
                var mockFile = new Mock<IFile>();
                mockFile.Setup(f => f.Name).Returns($"File_{i}_{j}.txt");
                mockFile.Setup(f => f.ServerRelativeUrl).Returns($"/sites/me/Documents/folder_{i}/File_{i}_{j}.txt");
                mockFiles.Add(mockFile.Object);
            }

            mockFileCollection.Setup(m => m.GetEnumerator()).Returns(() => mockFiles.GetEnumerator());
            mockFolder.Setup(f => f.Files).Returns(mockFileCollection.Object);
            subfolders.Add(mockFolder.Object);
        }

        // Setup folder collection to return our mock folders  
        var mockFolderCollection = new Mock<IFolderCollection>();
        mockFolderCollection.Setup(m => m.GetEnumerator()).Returns(() => subfolders.GetEnumerator());
        mockRootFolder.Setup(f => f.Folders).Returns(mockFolderCollection.Object);


        // Setup the mock list to return the root folder and the correct item count  
        mockList.Setup(l => l.RootFolder).Returns(mockRootFolder.Object);

        // Setup the mock web to return the mock list  
        mockWeb.Setup(web => web.Lists.GetByTitleAsync(It.IsAny<string>(), It.IsAny<Expression<Func<IList, object>>>()))
               .ReturnsAsync(mockList.Object);


        //mockList.Setup(l => l.RootFolder).Returns(mockRootFolder.Object);
        //mockWeb.Setup(web => web.Lists.GetByTitleAsync(It.IsAny<string>(), It.IsAny<Expression<Func<IList, object>>>()))
        //      .ReturnsAsync(mockList.Object);


        _output.WriteLine($"Total file list count {mockList.Object.ItemCount}");
        return mockList.Object;
    }
}

2. 调试即时窗口输出

mockFolderCollection
{Mock<IFolderCollection:1>}
    Behavior: Default
    CallBase: false
    DefaultValue: Empty
    DefaultValueProvider: {Moq.EmptyDefaultValueProvider}
    Invocations: {Moq.InvocationCollection}
    Name: "Mock<IFolderCollection:1>"
    Object (Moq.Mock): {Mock<IFolderCollection:1>.Object}
    Object: {Mock<IFolderCollection:1>.Object}
    Setups: {Moq.SetupCollection}
    Switches: Default

3. 被测方法代码

private async Task ProcessFolderAsync(IFolder folder, SharePointDocLibraryInventory result, bool recursive, string checksumType)
{
    // Process files in the current folder
    foreach (var file in folder.Files)
    {
        string checksum = await GenerateChecksumAsync(file, checksumType);
        result.Files.Add(new SynchronizedSharePointDocument
        {
            Name = file.Name,
            FullPath = file.ServerRelativeUrl,
            RelativePath = file.ServerRelativeUrl.Replace(folder.ServerRelativeUrl, "").TrimStart('/'),
            Checksum = checksum
        });
        result.TotalFiles++;
    }

    // Optionally process subfolders recursively
    if (recursive)
    {
        await folder.LoadAsync(f => f.Folders);
        foreach (var subfolder in folder.Folders)
        {
            result.TotalSubdirectories++;
            await ProcessFolderAsync(subfolder, result, recursive, checksumType);
        }
    }
}

问题分析与修复方案

核心问题

  1. 未处理LoadAsync的Mock设置:被测方法中调用了await folder.LoadAsync(f => f.Folders),但原Mock未定义该方法的行为,导致加载后文件夹集合未正确初始化。
  2. PnP对象未关联到Context:PnP Core的对象(如IFolder、IList)需要关联到IPnPContext才能正确执行异步操作,原代码未设置这一关联。
  3. 文件夹集合Mock不完整:仅设置了枚举器,未处理集合的其他必要属性(如Count),且未确保子文件夹的Folders属性返回空集合。

修改后的测试工厂代码

public IList SetupMockSharePointLibrary()
{
    var mockList = new Mock<IList>();
    var mockRootFolder = new Mock<IFolder>();
    var mockWeb = new Mock<IWeb>();
    var subfolders = new List<IFolder>();

    // 关联根文件夹到PnP Context
    mockRootFolder.Setup(f => f.Context).Returns(_mockPnpContext.Object);

    for (int i = 0; i < 3; i++)
    {
        var mockFolder = new Mock<IFolder>();
        var mockFileCollection = new Mock<IFileCollection>();
        var mockFiles = new List<IFile>();

        // 关联子文件夹到PnP Context
        mockFolder.Setup(f => f.Context).Returns(_mockPnpContext.Object);

        for (int j = 0; j < 2; j++)
        {
            var mockFile = new Mock<IFile>();
            mockFile.Setup(f => f.Name).Returns($"File_{i}_{j}.txt");
            mockFile.Setup(f => f.ServerRelativeUrl).Returns($"/sites/me/Documents/folder_{i}/File_{i}_{j}.txt");
            mockFiles.Add(mockFile.Object);
        }

        // 设置文件集合的枚举器
        mockFileCollection.Setup(m => m.GetEnumerator()).Returns(() => mockFiles.GetEnumerator());
        mockFolder.Setup(f => f.Files).Returns(mockFileCollection.Object);

        // 设置子文件夹的空子目录集合及LoadAsync行为
        var emptySubfolderCollection = new Mock<IFolderCollection>();
        emptySubfolderCollection.Setup(m => m.GetEnumerator()).Returns(() => new List<IFolder>().GetEnumerator());
        mockFolder.Setup(f => f.Folders).Returns(emptySubfolderCollection.Object);
        mockFolder.Setup(f => f.LoadAsync(It.IsAny<Expression<Func<IFolder, object>>>(), It.IsAny<CancellationToken>()))
                  .Returns(Task.CompletedTask);

        subfolders.Add(mockFolder.Object);
    }

    // 设置根文件夹的子目录集合
    var mockFolderCollection = new Mock<IFolderCollection>();
    mockFolderCollection.Setup(m => m.GetEnumerator()).Returns(() => subfolders.GetEnumerator());
    mockFolderCollection.Setup(m => m.Count).Returns(subfolders.Count);

    // 设置根文件夹的Folders属性及LoadAsync行为
    mockRootFolder.Setup(f => f.Folders).Returns(mockFolderCollection.Object);
    mockRootFolder.Setup(f => f.LoadAsync(It.IsAny<Expression<Func<IFolder, object>>>(), It.IsAny<CancellationToken>()))
                  .Returns(Task.CompletedTask);

    // 设置列表的根文件夹及Context关联
    mockList.Setup(l => l.RootFolder).Returns(mockRootFolder.Object);
    mockList.Setup(l => l.Context).Returns(_mockPnpContext.Object);

    // 设置Web返回列表及Context关联
    mockWeb.Setup(web => web.Lists.GetByTitleAsync(It.IsAny<string>(), It.IsAny<Expression<Func<IList, object>>>(), It.IsAny<CancellationToken>()))
           .ReturnsAsync(mockList.Object);
    mockWeb.Setup(w => w.Context).Returns(_mockPnpContext.Object);

    // 关联Web到PnP Context
    _mockPnpContext.Setup(c => c.Web).Returns(mockWeb.Object);

    _output.WriteLine($"Total file list count {mockList.Object.ItemCount}");
    return mockList.Object;
}

关键修复点

  • 所有PnP对象(IWeb、IList、IFolder)都关联到Mock的IPnPContext,确保异步方法正常触发Mock行为。
  • 为IFolder的LoadAsync方法设置空实现,避免调用时抛出异常。
  • 为子文件夹设置空的子目录集合,防止递归处理时出现空引用。
  • 补充文件夹集合的Count属性设置,覆盖更多接口行为。

内容的提问来源于stack exchange,提问作者dot

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.25 05:54:56