【问题标题】:How to test that a service method was not called如何测试未调用服务方法
【发布时间】:2012-06-20 23:42:19
【问题描述】:

我和我的同事正在讨论编写单元测试的正确方法,以确保用户在我们的 ASP.NET MVC 2 应用程序的表单中输入错误数据时收到错误。以下是我们过去所做的,从模型开始:

public class LoginModel
{
    public string Username { get; set; }
}

这是控制器动作:

[HttpPost]
public ActionResult Login( LoginModel loginModel )
{
    if ( loginModel.Username == null )
    {
        ModelState.AddModelError( "Username", "Username is required!" );

        return View( loginModel );
    }

    LoginService.Login( loginModel );
}

最后,测试方法如下:

[TestMethod]
public void Login_Post_Blank_Username_Displays_Error()
{
    var controller = GetHomeController();

    var loginModel = new LoginModel
    {
        Username = null
    };

    var result = controller.Login( loginModel );

    Assert.IsInstanceOfType( result, typeof( ViewResult ) );

    var view = (ViewResult)result;

    Assert.IsNotNull( view.ViewData.ModelState["Username"].Errors.First().ErrorMessage );
}

他向我指出,这确实不是针对这种情况编写测试的正确方法。出于一个原因,它非常脆弱 - 将 Username 属性更改为其他任何东西都会破坏测试。其次,最好依靠 DataAnnotations 并针对控制器正在做的事情进行测试。因此,我们的新模型如下所示:

public class LoginModel
{
    [Required( ErrorMessage = "Username is required!" )]
    public string Username { get; set; }
}

我们的控制器动作会变成这样:

[HttpPost]
public ActionResult Login( LoginModel loginModel )
{
    if ( !Model.IsValid() )
    {
        return View( loginModel );
    }

    LoginService.Login( loginModel );
}

问题出在单元测试上,它完全没有注意到 DataAnnotations,所以测试失败了。我的同事说我们真正应该测试的是没有调用 LoginService,但我不确定如何测试。他建议像这样使用起订量:

[TestMethod]
public void Login_Post_Blank_Username_Displays_Error()
{
    var controller = GetHomeController();

    var loginModel = new LoginModel
    {
        Username = null
    };

    loginServiceMock.Setup( x => x.Login( It.IsAny<LoginModel>() ) )
        .Callback( () => Assert.Fail( "Should not call LoginService if Username is blank!" ) );

    var result = controller.Login( loginModel );

    loginServiceMock.Verify();
}

您对此有何看法?测试未调用服务方法的正确方法是什么?在用户表单中测试不良数据的正确方法是什么?

【问题讨论】:

    标签: asp.net-mvc unit-testing moq mstest data-annotations


    【解决方案1】:

    验证时:

    loginServiceMock.Verify( x => x.Login( It.IsAny<LoginModel>() ), Times.Never() );
    

    您应该删除模拟中的设置。 为了让代码在你的action中进入if语句,你可以在控制器中调用action之前在ModelState中添加一个模型错误。

    你的代码应该是这样的:

    [TestMethod]
    public void Login_Post_Blank_Username_Displays_Error()
    {
        var controller = GetHomeController();
    
        var loginModel = new LoginModel
        {
            Username = null
        };
    
        controller.ModelState.AddModelError("a key", "a value");
    
        var result = controller.Login( loginModel );
    
        loginServiceMock.Verify( x => x.Login( It.IsAny<LoginModel>() ), Times.Never() );
    }
    

    【讨论】:

    • uvita,我希望看到代码按照您的建议在操作中输入 if 语句。这样我就知道它是正确的验证。但是,在测试中添加模型错误的目的是什么?那样不是违背了目的吗?
    • 这发生在幕后,模型绑定负责验证模型。在测试控制器操作时,您不应该关心这一点,这就是关注点分离的全部意义所在。
    • uvita,我明白你的意思。我认为这与我的同事说我们应该依赖 DataAnnotations 并仅针对控制器正在执行的操作进行测试时所提出的观点相同。
    【解决方案2】:

    假设您的 Mocked LoginService 已通过属性注入设置,您可以编写如下测试..

        [TestMethod]
        public void LoginPost_WhenUserNameIsNull_VerifyLoginMethodHasNotBeenCalled()
        {
            //Arrange
            var loginServiceMock = new Mock<ILoginService>();
            var sut = new HomeController { LoginService = loginServiceMock .Object};
    
            var loginModel = new LoginModel {
                Username = null
            };
            sut.ModelState.AddModelError("fakeKey", "fakeValue");
    
            //Act
            sut.Login(loginModel);
    
            //Verify
            loginServiceMock.Verify(x => x.Login(loginModel), Times.Never(), "Login method has been called.");
        }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-01-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-01-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多