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

如何用NSubstitute为带双参数的Controller Delete方法编写单元测试及测试场景

嘿,作为单元测试新手,给Controller的Delete动作写测试其实没那么复杂,咱们一步步拆解来做~

为Controller的Delete动作编写单元测试(基于NSubstitute)

首先,咱们的核心目标是测试Controller的Delete逻辑是否符合预期:包括正确调用仓储的删除方法、异常时正确记录日志,以及无论成功失败都返回跳转结果。下面是具体的实现步骤和测试场景:

准备工作

先确保你的测试项目装了这些NuGet包:

  • NSubstitute:用来模拟IRepository依赖
  • xunit(或者NUnit/MSTest,选你习惯的测试框架)
  • Microsoft.AspNetCore.Mvc.Testing:帮咱们验证Action的返回结果

测试代码示例

假设你的Controller叫DocumentController,下面是用xunit+NSubstitute写的测试用例:

using Xunit;
using NSubstitute;
using Microsoft.AspNetCore.Mvc;

public class DocumentControllerTests
{
    // 模拟的仓储依赖
    private readonly IRepository<CloudBlobContainer> _mockRepo;
    // 要测试的Controller实例
    private readonly DocumentController _controller;

    public DocumentControllerTests()
    {
        // 创建模拟的仓储对象
        _mockRepo = Substitute.For<IRepository<CloudBlobContainer>>();
        // 注入模拟依赖,实例化Controller
        _controller = new DocumentController(_mockRepo);
    }

    /// <summary>
    /// 测试正常流程:参数有效时,仓储方法被正确调用,且返回跳转结果
    /// </summary>
    [Fact]
    public void Delete_ValidParams_CallsRepoDeleteAndRedirectsToIndex()
    {
        // 准备测试参数
        var testFileName = "report.pdf";
        var testIsPublic = true;

        // 执行Delete动作
        var result = _controller.Delete(testFileName, testIsPublic) as RedirectToActionResult;

        // 验证仓储的DeleteDocument方法被调用,且传入的参数完全匹配
        _mockRepo.Received().DeleteDocument(Arg.Is<PortalDocument>(doc => 
            doc.Path == testFileName && doc.IsPublic == testIsPublic));
        
        // 验证返回的是跳转到Index视图的结果
        Assert.NotNull(result);
        Assert.Equal(DocumentController.IndexViewName, result.ActionName);
    }

    /// <summary>
    /// 测试异常流程:仓储抛出异常时,日志被记录,且依然返回跳转结果
    /// </summary>
    [Fact]
    public void Delete_RepoThrowsException_LogsErrorAndStillRedirects()
    {
        // 准备测试数据和异常
        var testFileName = "private-note.txt";
        var testIsPublic = false;
        var testException = new Exception("Failed to delete document");

        // 配置模拟仓储:调用DeleteDocument时抛出异常
        _mockRepo.When(repo => repo.DeleteDocument(Arg.Any<PortalDocument>()))
                 .Do(_ => throw testException);

        // 模拟日志(这里假设你把EventLog.Logger改成了可注入的ILogger,静态日志不好测试)
        var mockLogger = Substitute.For<ILogger<DocumentController>>();
        // 如果你的Controller是通过构造函数注入日志的,这里要把mockLogger也传进去
        _controller = new DocumentController(_mockRepo, mockLogger);

        // 执行Delete动作
        var result = _controller.Delete(testFileName, testIsPublic) as RedirectToActionResult;

        // 验证日志的LogCritical方法被正确调用
        mockLogger.Received().LogCritical(testException, testException.Message);
        
        // 验证即使出错,依然返回跳转结果
        Assert.NotNull(result);
        Assert.Equal(DocumentController.IndexViewName, result.ActionName);
    }
}

必须覆盖的测试场景

你需要确保这几个核心场景都被覆盖:

  • 正常成功场景:传入有效参数,验证仓储方法被正确调用(参数完全匹配),且Controller返回正确的跳转。
  • 异常处理场景:仓储抛出异常时,验证日志被记录,同时Controller不会崩溃,依然返回跳转结果。
  • 参数边界场景:测试fileName为空、null,或者isPublic为false的情况,确认仓储收到的PortalDocument参数和传入的一致(毕竟当前Controller没有做参数校验,直接传给了仓储)。

    小提示:如果之后要给Controller加参数校验(比如fileName不能为空),那还要加对应的测试用例,验证非法参数时的处理逻辑。

几个关键注意事项

  1. Controller要支持依赖注入:你的Controller构造函数必须接收IRepository<T>作为参数,这样才能注入模拟实例。如果当前没有,赶紧调整:
    public class DocumentController : Controller
    {
        private readonly IRepository<CloudBlobContainer> _repo;
        private readonly ILogger<DocumentController> _logger;
        public const string IndexViewName = "Index";
    
        // 注入仓储和日志
        public DocumentController(IRepository<CloudBlobContainer> repo, ILogger<DocumentController> logger)
        {
            _repo = repo;
            _logger = logger;
        }
    
        // 你的Delete动作代码(记得把EventLog.Logger换成注入的_logger)
        public ActionResult Delete(string fileName, bool isPublic) 
        { 
            try 
            { 
                _repo.DeleteDocument(new PortalDocument { Path = fileName, IsPublic = isPublic }); 
            } 
            catch (Exception e) 
            { 
                _logger.LogCritical(e, e.Message); 
            } 
            return RedirectToAction(IndexViewName); 
        }
    }
    
  2. 避免静态日志:静态的EventLog.Logger会让测试变得麻烦,最好改成依赖注入ILogger<T>的方式,这样更容易模拟和验证日志行为。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.27 07:03:30