如何用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不能为空),那还要加对应的测试用例,验证非法参数时的处理逻辑。
几个关键注意事项
- 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); } } - 避免静态日志:静态的
EventLog.Logger会让测试变得麻烦,最好改成依赖注入ILogger<T>的方式,这样更容易模拟和验证日志行为。
内容的提问来源于stack exchange,提问作者Nishi
相关产品推荐
相关产品推荐

