简体   繁体   中英

How to test that a service method was not called

My coworker and I were discussing the correct way to write a unit test to make sure a user receives an error when entering bad data into a form in our ASP.NET MVC 2 application. Here's what we've done in the past, starting with the model:

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

Here's the controller action:

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

        return View( loginModel );
    }

    LoginService.Login( loginModel );
}

Finally, here's the test method:

[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 );
}

He pointed out to me that this is really not the correct way to write a test for this situation. For one reason, it's extremely brittle - changing the Username property to anything else will break the test. Second, it would be better to rely on DataAnnotations and just test against what the controller is doing. So, our new model would look like this:

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

And our controller action would change to this:

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

    LoginService.Login( loginModel );
}

The problem comes with the unit test, which is totally oblivious to DataAnnotations, so the test fails. My coworker said that what we should REALLY be testing is that the LoginService is NOT called, but I'm not sure how to test for this. He suggested using Moq like this:

[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();
}

What are your thoughts on this? What's the correct way to test that a service method was NOT called? What about the correct way to test for bad data in a user form?

When verifying:

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

You should remove the setup in the mock. In order to make the code enter the if statement in your action, you can add a model error in the ModelState before invoking the action in the controller.

Here's how your code should look like:

[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() );
}

Assuming that your Mocked LoginService has been set via property injection, you can write the test as below..

    [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.");
    }

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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