Search code examples
asp.net-mvcunit-testingasp.net-mvc-5asp.net-identitymoq

How to mock a method and return a result that's set by its callback?


I'm using the code below to mock a UserManager.

The problem I'm having is that oResult remains null. But I'm unable to give it a value outside of the callback, as a password is only available inside the callback. I can't just return a predetermined value (e.g. IdentityResult.Success), as the result must be generated at runtime.

These three questions are similar, but they don't exactly cover the issue:

  1. validate MOQ unit test method return value (this is the closest)
  2. mocking a method using Moq framework doesn't return expected result
  3. Moq setting method return value

The difference between all of these and my situation is that in mine the method's required input value is only available within the callback.

I'm using this to test the behaviors of my controller(s).

How can I execute a callback for a mocked method (CreateAsync() in this case) and return its result?


Protected Function UserManagerMock(Of TUser As Db.User, TCity as Db.City)(Users As List(Of TUser)) As Mock(Of UserManager)
  Dim oManagerMock As Mock(Of UserManager)
  Dim oStoreMock As Mock(Of IUserStore(Of TUser))
  Dim oCallback As Action(Of TUser, String)
  Dim oManager As UserManager
  Dim oResult As IdentityResult
  Dim oSetup As Expression(Of Func(Of UserManager, Task(Of IdentityResult)))

  oStoreMock = New Mock(Of IUserStore(Of TUser))
  oManagerMock = New Mock(Of UserManager)(oUserStoreMock.Object)
  oManager = oUserManagerMock.Object
  oCallback = Sub(User, Password, City)
                oResult = oManager.PasswordValidator.ValidateAsync(Password).Result

                If oResult Is IdentityResult.Success Then
                  User.PasswordHash = oManager.PasswordHasher.HashPassword(Password)
                  Users.Add(User)
                End If
              End Sub

  oManager.PasswordValidator = New PasswordValidator
  oManager.UserValidator = New UserValidator(Of TUser)(oManager)

  oSetup = Function(Manager) Manager.CreateAsync(It.IsAny(Of Db.User), It.IsAny(Of String), It.IsAny(Of Db.City))
  oUserManagerMock.Setup(oSetup).ReturnsAsync(oResult).Callback(oCallback)

  Return oManagerMock
End Function

Solution

  • This can be accomplished by doing away with the callback altogether, and instead using a

    Func(Of ..., Task(Of IdentityResult))
    

    in the setup's Returns() call.

    Example:

    Protected Function GetUserManager(Users As List(Of Db.User)) As UserManager
      Return Me.GetUserManagerMock(Of Db.User, Db.City)(Users).Object
    End Function
    
    Private Function GetUserManagerMock(Of TUser As Db.User, TCity As Db.City)(Users As List(Of TUser)) As Mock(Of UserManager)
      Dim oManagerMock As Mock(Of UserManager)
      Dim oStoreMock As Mock(Of IUserStore(Of TUser))
      Dim oManager As UserManager
      Dim oReturn As Func(Of TUser, String, TCity, Task(Of IdentityResult))
      Dim oResult As IdentityResult
      Dim oSetup As Expression(Of Func(Of UserManager, Task(Of IdentityResult)))
    
      oStoreMock = New Mock(Of IUserStore(Of TUser))
      oManagerMock = New Mock(Of UserManager)(oUserStoreMock.Object)
      oManager = oUserManagerMock.Object
    
      oReturn = Async Function(User, Password, City)
                  If City.IsNothing Then
                    oResult = IdentityResult.Failed($"{NameOf(City)} is required.")
                  Else
                    oResult = Await oManager.UserValidator.ValidateAsync(User)
    
                    If oResult Is IdentityResult.Success Then
                      oResult = Await oManager.PasswordValidator.ValidateAsync(Password)
    
                      If oResult Is IdentityResult.Success Then
                        User.PasswordHash = oManager.PasswordHasher.HashPassword(Password)
                        Users.Add(User)
    
                        User.CityId = City.Id
                        User.City = City
    
                        City.Users.Add(User)
                      End If
                    End If
                  End If
    
                  Return oResult
                End Function
    
      oSetup = Function(Manager) Manager.CreateAsync(It.IsAny(Of Db.User), It.IsAny(Of String), It.IsAny(Of Db.City))
      oManagerMock.Setup(oSetup).Returns(oReturn)
    
      Return oManagerMock
    End Function
    

    Use it like this:

    <Fact>
    Public Async Function CreateUser() As Task
      Dim oUserManager As UserManager
      Dim oResult As IdentityResult
      Dim sPassword As String
      Dim oUser as Db.User
      Dim oCity as Db.City
      
      sPassword = "P@ssw0rd!"
      oUser = New Db.User With {.FirstName = "User", .LastName = "Name", .UserName = "user.name", .Email = "username@domain.com"}
      oCity = New Db.City With {.Id = 1, .Name = "BigCity", .Code = "L69CNV5"}
    
      oUserManager = Me.GetUserManager(Me.Users)
      oResult = Await oUserManager.CreateAsync(oUser, sPassword, oCity)
    
      Assert.True(oResult.Succeeded)
    End Function
    

    Source here. Thanks again to stakx.