繁体   English   中英

一个单元应该如何测试 .NET MVC 控制器?

[英]How should one unit test a .NET MVC controller?

我正在寻找有关 .NET mvc 控制器有效单元测试的建议。

在我工作的地方,许多这样的测试使用 moq 来模拟数据层并断言某些数据层方法被调用。 这对我来说似乎没什么用,因为它本质上是验证实现没有改变,而不是测试 API。

我还阅读了推荐诸如检查返回的视图模型类型是否正确的文章。 我可以看到它提供了一些价值,但仅凭它似乎并不值得编写多行模拟代码(我们的应用程序的数据模型非常庞大和复杂)。

谁能建议一些更好的控制器单元测试方法或解释为什么上述方法有效/有用?

谢谢!

控制器单元测试应该测试您的操作方法中的代码算法,而不是您的数据层。 这是模拟这些数据服务的原因之一。 控制器期望从存储库/服务/等接收某些值,并在从它们接收不同的信息时采取不同的行动。

您编写单元测试来断言控制器在非常特定的场景/情况下以非常特定的方式运行。 您的数据层是为控制器/操作方法提供这些情况的应用程序的一部分。 断言服务方法被控制器调用是有价值的,因为您可以确定控制器从另一个地方获取信息。

检查返回的视图模型的类型是有价值的,因为如果返回错误的视图模型类型,MVC 将抛出运行时异常。 您可以通过运行单元测试来防止这种情况在生产中发生。 如果测试失败,那么视图可能会在生产中抛出异常。

单元测试很有价值,因为它们使重构更容易。 您可以更改实现,并通过确保所有单元测试通过来断言行为仍然相同。

回复评论 #1

如果更改被测方法的实现需要更改/删除下层模拟方法,那么单元测试也必须更改。 但是,这不应该像您想象的那样经常发生。

典型的红绿重构工作流要求在编写单元测试之前编写单元测试。 (这意味着在短时间内,您的测试代码将无法编译,这也是许多年轻/缺乏经验的开发人员难以采用红绿重构的原因。)

如果您首先编写单元测试,您将知道控制器需要从较低层获取信息。 您如何确定它会尝试获取该信息? 通过模拟提供信息的下层方法,并断言控制器调用了下层方法。

当我使用“改变实施”这个词时,我可能说错了。 当必须更改控制器的操作方法和相应的单元测试以更改或删除模拟方法时,您实际上是在更改控制器的行为。 根据定义,重构意味着在不改变整体行为和预期结果的情况下改变实现。

红绿重构是一种质量保证方法,有助于防止代码中的错误和缺陷出现。 通常,开发人员会在错误出现后更改实现以将其删除。 所以重申一下,你担心的情况不应该像你想象的那样经常发生。

你应该首先让你的控制器节食。 然后你就可以愉快地对它们进行单元测试了。 如果它们很胖并且你已经把所有的业务逻辑都塞进它们里面,我同意你会在你的单元测试中传递你的生活嘲笑的东西,并抱怨这是浪费时间。

当你谈论复杂的逻辑时,这并不一定意味着这个逻辑不能在不同的层中分离并且每个方法都单独进行单元测试。

是的,您应该一直测试到数据库。 您投入到模拟中的时间更少,并且您从模拟中获得的价值也非常少(系统中 80% 的可能错误无法通过模拟来选择)。

当您从控制器一直测试到数据库或 Web 服务时,它不称为单元测试,而是集成测试。 我个人相信集成测试而不是单元测试(即使它们都用于不同的目的)。 而且我能够通过集成测试(场景测试)成功地进行测试驱动开发。

这是我们团队的工作方式。 开始时的每个测试类都会重新生成数据库并使用最少的数据集(例如:用户角色)填充/播种表。 基于控制器的需要,我们填充数据库并验证控制器是否完成它的任务。 这样做的目的是使其他方法留下的 DB 损坏数据永远不会使测试失败。 除了运行所需的时间之外,几乎所有单元测试的质量(即使它是一个理论)都是可以获得的。 使用容器可以减少顺序运行所需的时间。 同样对于容器,我们不需要重新创建数据库,因为每个测试都会在容器中获得自己的新数据库(将在测试后删除)。

在我的职业生涯中,只有 2% 的情况(或很少)当我被迫使用模拟/存根时,因为无法创建更真实的数据源。 但在所有其他情况下,集成测试是可能的。

使用这种方法,我们需要时间才能达到成熟的水平。 我们有一个很好的框架来处理测试数据的填充和检索(一等公民)。 它得到了很大的回报! 第一步是告别模拟和单元测试。 如果模拟没有意义,那么它们不适合您! 集成测试让您睡个好觉。

====================================

在下面的评论后编辑:演示

集成测试或功能测试必须直接处理 DB/源。 没有嘲笑。 所以这些是步骤。 您想测试getEmployee( emp_id) 下面的所有这 5 个步骤都是在一个测试方法中完成的。

  1. 删除数据库

  2. 创建数据库并填充角色和其他基础设施数据

  3. 创建带有 ID 的员工记录

  4. 使用这个ID并调用getEmployee(emp_id) //这可以是一个api-url调用(这样db连接字符串就不需要在测试项目中维护,我们只需更改域名就可以测试几乎所有环境)

  5. 现在 Assert()/ 验证返回的数据是否正确

    这证明getEmployee()有效。 第 3 步之前的步骤要求您拥有仅供测试项目使用的代码。 第 4 步调用应用程序代码。 我的意思是创建员工(第 2 步)应该通过测试项目代码而不是应用程序代码来完成。 如果有用于创建员工的应用程序代码(例如: CreateEmployee() ),则不应使用此代码。 同样,当我们测试CreateEmployee() 时,不应使用GetEmployee()应用程序代码。 我们应该有一个用于从表中获取数据的测试项目代码。

这样就没有嘲笑了! 删除和创建 DB 的原因是为了防止 DB 有损坏的数据。 使用我们的方法,无论我们运行多少次,测试都会通过。

特别提示:在第 5 步 getEmployee() 返回一个员工对象。 如果稍后开发人员删除或更改字段名称,则测试中断。 如果开发人员稍后添加新字段怎么办? 他/她忘记为其添加测试(断言)? 测试不会捡起来。 解决方案是添加字段计数检查。 例如:Employee 对象有 4 个字段(First Name、Last Name、Designation、Sex)。 因此员工对象的Assert 字段数为4。所以当添加新字段时,我们的测试会因为计数失败并提醒开发人员为新添加的字段添加一个assert 字段。

这是一篇很棒的文章,讨论了集成测试相对于单元测试的好处,因为“单元测试会杀死!” (它说)

单元测试的重点是根据一组条件单独测试方法的行为。 您使用模拟设置测试的条件,并通过检查它如何与周围的其他代码交互来断言方法的行为——通过检查它尝试调用哪些外部方法,但特别是通过检查给定条件下它返回的值。

因此,对于返回 ActionResults 的 Controller 方法,检查返回的 ActionResult 的值非常有用。

查看此处的“为控制器创建单元测试”部分,了解使用 Moq 的一些非常清晰的示例

这是来自该页面的一个很好的示例,它测试当控制器尝试创建联系人记录但失败时返回适当的视图。

[TestMethod]
public void CreateInvalidContact()
{
    // Arrange
    var contact = new Contact();
    _service.Expect(s => s.CreateContact(contact)).Returns(false);
    var controller = new ContactController(_service.Object);

    // Act
    var result = (ViewResult)controller.Create(contact);

    // Assert
    Assert.AreEqual("Create", result.ViewName);
}

我认为对控制器进行单元测试没有多大意义,因为它通常只是连接其他部分的一段代码。 单元测试通常包括大量模拟,只是验证其他服务是否正确连接。 测试本身是实现代码的反映。

我更喜欢集成测试——我不是从一个具体的控制器开始,而是从一个 Url 开始,并验证返回的模型是否具有正确的值。 Ivonna的帮助下,测试可能如下所示:

var response = new TestSession().Get("/Users/List");
Assert.IsInstanceOf<UserListModel>(response.Model);

var model = (UserListModel) response.Model;
Assert.AreEqual(1, model.Users.Count);

我可以模拟数据库访问,但我更喜欢不同的方法:设置 SQLite 的内存实例,并在每个新测试中重新创建它,以及所需的数据。 它使我的测试足够快,但不是复杂的模拟,而是让它们清楚,例如,只需创建并保存一个 User 实例,而不是模拟UserService (这可能是一个实现细节)。

通常,当您谈论单元测试时,您正在测试一个单独的过程或方法,而不是整个系统,同时试图消除所有外部依赖项。

换句话说,在测试控制器时,您正在逐个编写测试方法,您甚至不需要加载视图或模型,这些是您应该“模拟”的部分。 然后,您可以更改模拟以返回在其他测试中难以重现的值或错误。

我通常遵循ASP.NET Core本指南:

https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/testing?view=aspnetcore-5.0

代码示例:

https://github.com/dotnet/AspNetCore.Docs/tree/master/aspnetcore/mvc/controllers/testing/samples/

例子:

控制器:

public class HomeController : Controller
{
    private readonly IBrainstormSessionRepository _sessionRepository;

    public HomeController(IBrainstormSessionRepository sessionRepository)
    {
        _sessionRepository = sessionRepository;
    }

    public async Task<IActionResult> Index()
    {
        var sessionList = await _sessionRepository.ListAsync();

        var model = sessionList.Select(session => new StormSessionViewModel()
        {
            Id = session.Id,
            DateCreated = session.DateCreated,
            Name = session.Name,
            IdeaCount = session.Ideas.Count
        });

        return View(model);
    }

    public class NewSessionModel
    {
        [Required]
        public string SessionName { get; set; }
    }

    [HttpPost]
    public async Task<IActionResult> Index(NewSessionModel model)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }
        else
        {
            await _sessionRepository.AddAsync(new BrainstormSession()
            {
                DateCreated = DateTimeOffset.Now,
                Name = model.SessionName
            });
        }

        return RedirectToAction(actionName: nameof(Index));
    }
}

单元测试:

[Fact]
public async Task Index_ReturnsAViewResult_WithAListOfBrainstormSessions()
{
    // Arrange
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.ListAsync())
        .ReturnsAsync(GetTestSessions());
    var controller = new HomeController(mockRepo.Object);

    // Act
    var result = await controller.Index();

    // Assert
    var viewResult = Assert.IsType<ViewResult>(result);
    var model = Assert.IsAssignableFrom<IEnumerable<StormSessionViewModel>>(
        viewResult.ViewData.Model);
    Assert.Equal(2, model.Count());
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM