代码之家  ›  专栏  ›  技术社区  ›  Neil Barnwell

ASP.NET MVC中的单元测试:最小化每个测试的断言数

  •  1
  • Neil Barnwell  · 技术社区  · 14 年前

    我正在ASP.NET MVC的Greenfield Hobby应用程序上试用TDD,并开始使用以下测试方法:

    [Test]
    public void Index_GetRequest_ShouldReturnPopulatedIndexViewModel()
    {
        var controller = new EmployeeController();
        controller.EmployeeService = GetPrePopulatedEmployeeService();
    
        var actionResult = (ViewResult)controller.Index();
    
        var employeeIndexViewModel = (EmployeeIndexViewModel)actionResult.ViewData.Model;
        EmployeeDetailsViewModel employeeViewModel = employeeIndexViewModel.Items[0];
    
        Assert.AreEqual(1, employeeViewModel.ID);
        Assert.AreEqual("Neil Barnwell", employeeViewModel.Name);
        Assert.AreEqual("ABC123", employeeViewModel.PayrollNumber);
    }
    

    现在我知道理想的测试只有一个 Assert.xxx() 调用,但这是否意味着我应该重构上面的内容,以使用以下名称分隔测试:

    • 索引\u GetRequest \u shouldTurnPopulatedIndexViewModelwithCorrectId
    • index_getrequest_shouldreturnopulatedindexviewmodelwithcorrectname
    • 索引\u GetRequest \u shouldTurnPopulatedIndexViewModelwithCorrectPayRollNumber

    …如果大多数测试都是重复的代码(因此测试不止一次,并且违反了“保持测试快速”的建议)?这对我来说似乎是极端的,所以如果我是对的,那么“每次测试一次断言”建议的现实意义是什么?

    3 回复  |  直到 14 年前
        1
  •  2
  •   Damian Powell    14 年前

    RoyOsherove在他的《单元测试的艺术》一书中谈到了这个问题。他也赞成在单元测试中只测试一个事实,但他指出,这并不总是意味着只有一个断言。在这种情况下,您正在测试 GetRequest , the Index 方法 ShouldReturnPopulatedIndexViewModel . 在我看来 密集的 视图模型 应该 包含一个ID、一个名称, 在这个测试中断言所有这些事情的工资总额是非常明智的。

    但是,如果您真的想要分割断言(例如,如果您正在测试需要类似设置但逻辑上不相同的各个方面),那么您可以这样做,而不需要太多的努力:

    [Test] 
    public void Index_GetRequest_ShouldReturnPopulatedIndexViewModel() 
    {
        var employeeDetailsViewModel = SetupFor_Index_GetRequest();
        Assert.AreEqual(1, employeeDetailsViewModel.ID);
    }
    
    [Test] 
    public void Index_GetRequest_ShouldReturnPopulatedIndexViewModel() 
    {
        var employeeDetailsViewModel = SetupFor_Index_GetRequest();
        Assert.AreEqual("Neil Barnwell", employeeDetailsViewModel.Name); 
    }
    
    [Test] 
    public void Index_GetRequest_ShouldReturnPopulatedIndexViewModel() 
    {
        var employeeDetailsViewModel = SetupFor_Index_GetRequest();
        Assert.AreEqual("ABC123", employeeDetailsViewModel.PayrollNumber); 
    }
    
    private EmployeeDetailsViewModel SetupFor_Index_GetRequest()
    {
        var controller = new EmployeeController(); 
        controller.EmployeeService = GetPrePopulatedEmployeeService(); 
    
        var actionResult = (ViewResult)controller.Index(); 
    
        var employeeIndexViewModel = (EmployeeIndexViewModel)actionResult.ViewData.Model; 
        var employeeDetailsViewModel = employeeIndexViewModel.Items[0]; 
    
        return employeeDetailsViewModel;
    }
    

    也可以说,由于这些测试需要相同的设置,它们应该有自己的夹具,并有一个 [SetUp] 方法。不过,这种方法也有缺点。它可能导致比实际的、真实的类更多的单元测试类,这可能是不可取的。

        2
  •  3
  •   Adrian Grigore    14 年前

    对我来说,这似乎也很极端,这就是为什么我还为每个测试编写多个断言的原因。我已经有了500个测试,每个测试只写一个断言就可以将其放大到至少2500个,而我的测试需要10分钟才能运行。

    由于一个优秀的REST运行者(如Resharper)可以让您看到测试很快失败的线路,所以您仍然可以很容易地理解为什么测试失败。如果您不介意额外的工作,您还可以添加断言描述(“断言工资单编号正确”),这样您甚至可以在不查看源代码的情况下看到这一点。有了这一点,每个测试只剩下一个断言就没有什么理由了。

        3
  •  1
  •   AbstractCode    14 年前

    我使用一个助手类来包含断言。这使测试方法保持整洁,并将重点放在它们实际试图建立的内容上。它看起来像:

    public static class MvcAssert
    {
        public static void IsViewResult(ActionResult actionResult)
        {
            Assert.IsInstanceOfType<ViewResult>(actionResult);
        }
    
        public static void IsViewResult<TModel>(ActionResult actionResult, TModel model)
        {
            Assert.IsInstanceOfType<ViewResult>(actionResult);
            Assert.AreSame(model, ((ViewResult) actionResult).ViewData.Model);
        }
    
        public static void IsViewResult<TModel>(ActionResult actionResult, Func<TModel, bool> modelValidator)
            where TModel : class
        {
            Assert.IsInstanceOfType<ViewResult>(actionResult);
            Assert.IsTrue(modelValidator(((ViewResult) actionResult).ViewData.Model as TModel));
        }
    
        public static void IsRedirectToRouteResult(ActionResult actionResult, string action)
        {
            var redirectToRouteResult = actionResult as RedirectToRouteResult;
            Assert.IsNotNull(redirectToRouteResult);
            Assert.AreEqual(action, redirectToRouteResult.RouteValues["action"]);
        }
    }