没有为重载的模拟方法引发回调

Callback not being raised for an overloaded mocked method

我正在构建一个多租户网站,因此我需要重载 UserManager.CreateAsync() 以接受类型为 City 的附加参数。也就是说,每个User属于一个City。我相应地在 User 模型上配置了额外的属性。

问题是我无法让 Moq 引发重载模拟方法的回调。

这是我的模拟设置:

Protected Function GetUserManagerMock(Of TUser As Db.User)(Users As List(Of TUser), ExpectedResult As IdentityResult) As Mock(Of UserManager)
  Dim oCreateSetup As Expression(Of Func(Of UserManager, Task(Of IdentityResult)))
  Dim oDeleteSetup As Expression(Of Func(Of UserManager, Task(Of IdentityResult)))
  Dim oUpdateSetup As Expression(Of Func(Of UserManager, Task(Of IdentityResult)))
  Dim oManagerMock As Mock(Of UserManager)
  Dim oStoreMock As Mock(Of IUserStore(Of TUser))
  Dim oCallback As Action(Of TUser, String, Db.City)
  Dim oManager As UserManager
  Dim oResult As IdentityResult

  oStoreMock = New Mock(Of IUserStore(Of TUser))
  oManagerMock = New Mock(Of UserManager)(oStoreMock.Object)
  oManager = oManagerMock.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)

                  User.CityId = City.Id
                  User.City = City

                  City.Users.Add(User)
                End If
              End Sub

  oCreateSetup = Function(Manager) Manager.CreateAsync(It.IsAny(Of TUser), It.IsAny(Of String), It.IsAny(Of Db.City))
  oDeleteSetup = Function(Manager) Manager.DeleteAsync(It.IsAny(Of TUser))
  oUpdateSetup = Function(Manager) Manager.UpdateAsync(It.IsAny(Of TUser))

  oManagerMock.Setup(oCreateSetup).ReturnsAsync(ExpectedResult).Callback(oCallback)
  oManagerMock.Setup(oDeleteSetup).ReturnsAsync(ExpectedResult)
  oManagerMock.Setup(oUpdateSetup).ReturnsAsync(ExpectedResult)

  Return oManagerMock
End Function

这是重载的 UserManager 方法:

Public Overridable Overloads Async Function CreateAsync(User As Db.User, Password As String, City As Db.City) As Task(Of IdentityResult)
  Dim oResult As IdentityResult

  Password.ThrowIfNothing(NameOf(Password))
  User.ThrowIfNothing(NameOf(User))
  City.ThrowIfNothing(NameOf(City))

  User.CityId = City.Id
  User.City = City

  oResult = Await MyBase.CreateAsync(User, Password)

  If oResult.Succeeded Then
    City.Users.RemoveAll(Function(U) U.Id = User.Id)
    City.Users.Add(User)
  Else
    User.CityId = Nothing
    User.City = Nothing
  End If

  Return oResult
End Function

这是正在测试的控制器方法:

<HttpPost>
<ActionName("Create")>
<AllowAnonymous>
<ValidateAntiForgeryToken>
Public Async Function CreateAsync(Model As Welcome) As Task(Of ActionResult)
  Dim oIdentity As IdentityResult
  Dim oAction As ActionResult
  Dim oUser As Db.User

  If Me.ModelState.IsValid Then
    If Model.City.IsNothing Then
      Model.City = Me.TempData.Peek(NameOf(Db.City))
      oAction = Me.View("One", Model)
    Else
      oUser = New Db.User With {
        .FirstName = Model.FirstName,
        .LastName = Model.LastName,
        .UserName = Model.Username,
        .CityId = Model.City.Id,
        .City = Model.City
      }

      oIdentity = Await Me.UserManager.CreateAsync(oUser, Model.Password, Model.City)

      If oIdentity.Succeeded Then
        oAction = Me.View("Two", Model)
      Else
        oAction = Me.View("Three", Model)
      End If
    End If
  Else
    oAction = Me.View("Four", Model)
  End If

  Return oAction
End Function

...这是测试:

<Fact>
Public Async Function CreateUserSucceeds() As Task
  Dim oUserManager As UserManager
  Dim oController As UsersController
  Dim oViewModel As Welcome
  Dim oResult As ViewResult
  Dim oCity As Db.City

  Me.Users.Clear()
  Me.Cities.ForEach(Sub(City) City.Users.Clear())

  oCity = Me.Cities.First
  oUserManager = Me.UserManagerMock(Me.Users, IdentityResult.Success).Object
  oController = New UsersController(Me.Worker, Nothing, oUserManager, Nothing)
  oViewModel = New Welcome With {.City = oCity, .FirstName = "User", .LastName = "Name", .Password = "P@ssw0rd!"}
  oResult = TryCast(Await oController.CreateAsync(oViewModel), ViewResult)

  Assert.True(Me.Users.Count = 1)
  Assert.True(Me.Users.First.City.IsNotNothing)
  Assert.True(oCity.Code = Me.Users.First.City.Code)
End Function

执行会按预期跳过重载的 UserManager.CreateAsync() 方法,但回调 Action 永远不会 运行。控制器方法中的这行代码似乎被完全跳过了:

oIdentity = Await Me.UserManager.CreateAsync(oUser, Model.Password, Model.City)

因此,无论我做什么,oIdentity 都是 Nothingnull 在 C# 中)。

我尝试在打开 CallBase 的情况下创建模拟,如下所示:

oManagerMock = New Mock(Of UserManager)(oStoreMock.Object) With {.CallBase = True}

...但这导致重载方法本身是 运行,这当然不会。我正在 运行 进行单元测试,而不是集成测试。

谁能看出为什么没有调用此回调?

--编辑 1--

从那以后我发现不再为我模拟的 任何 方法引发回调——而不仅仅是重载方法。这非常奇怪,因为它过去是有效的。

我决定从备份中恢复代码并return它到它的工作状态。我很快就会有更多信息。

--编辑 2--

问题还没有解决,但我已经接近了一点。

当我将模拟的 UserManager 转换为身份框架 class(即 UserManager(Of TUser))时,成功引发了回调。但是,当我将它转换到我的子程序时class,为了获得重载方法,它失败了。

这就是为什么我最初的印象是只有重载方法导致了失败——因为我必须更改转换才能模拟该方法。铸造变化导致了问题,而不是过载。

我已经检查过 the source code for the Identity framework,但我没有发现任何与我的概念不同的东西。

这是我的完整 UserManager subclass:

Imports System
Imports System.Threading.Tasks
Imports Intexx
Imports Website.Models
Imports Microsoft.AspNet.Identity
Imports Microsoft.AspNet.Identity.EntityFramework
Imports Microsoft.AspNet.Identity.Owin
Imports Microsoft.Owin
Imports Microsoft.Owin.Security.DataProtection

Public Class UserManager
  Inherits UserManager(Of Db.User)

  Public Sub New(Store As IUserStore(Of Db.User))
    MyBase.New(Store)

    Me.PasswordValidator = New PasswordValidator
    Me.UserValidator = New UserValidator(Me)
  End Sub

  Public Shared Function Create(Options As IdentityFactoryOptions(Of UserManager), Context As IOwinContext) As UserManager
    Dim oPhoneProvider As PhoneNumberTokenProvider(Of Db.User)
    Dim oEmailProvider As EmailTokenProvider(Of Db.User)
    Dim oDataProtector As IDataProtector
    Dim oDataProvider As IDataProtectionProvider
    Dim oUserStore As IUserStore(Of Db.User)
    Dim oManager As UserManager
    Dim oContext As Db.Context

    oContext = Context.Get(Of Db.Context)
    oUserStore = New UserStore(Of Db.User)(oContext)

    oManager = New UserManager(oUserStore) With {
      .MaxFailedAccessAttemptsBeforeLockout = 5,
      .DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(5),
      .UserLockoutEnabledByDefault = True
    }

    ' Create the two-factor authentication providers
    oPhoneProvider = New PhoneNumberTokenProvider(Of Db.User) With {.MessageFormat = "Your security code is {0}"}
    oEmailProvider = New EmailTokenProvider(Of Db.User) With {.Subject = "Security Code", .BodyFormat = "Your security code is {0}"}

    ' Register the two-factor authentication providers. This application
    ' uses Phone and Email as a step of receiving a code for verifying
    ' the user. You can write your own provider and plug it in here.
    oManager.RegisterTwoFactorProvider("Phone Code", oPhoneProvider)
    oManager.RegisterTwoFactorProvider("Email Code", oEmailProvider)

    oDataProvider = Options.DataProtectionProvider

    If oDataProvider.IsNotNothing Then
      oDataProtector = oDataProvider.Create("ASP.NET Identity")
      oManager.UserTokenProvider = New DataProtectorTokenProvider(Of Db.User)(oDataProtector)
    End If

    oManager.EmailService = New EmailService
    oManager.SmsService = New SmsService

    Return oManager
  End Function

  Public Overridable Overloads Async Function CreateAsync(User As Db.User, Password As String, City As Db.City) As Task(Of IdentityResult)
    Dim oResult As IdentityResult

    Password.ThrowIfNothing(NameOf(Password))
    User.ThrowIfNothing(NameOf(User))
    City.ThrowIfNothing(NameOf(City))

    User.CityId = City.Id
    User.City = City

    oResult = Await MyBase.CreateAsync(User, Password)

    If oResult.Succeeded Then
      City.Users.RemoveAll(Function(U) U.Id = User.Id)
      City.Users.Add(User)
    Else
      User.CityId = Nothing
      User.City = Nothing
    End If

    Return oResult
  End Function
End Class

目前,由于我似乎只能在使用本机转换时引发回调,临时解决方法可能是使用该转换并在回调中检查 User.City = Nothing。至少我可以解除封锁。

但是为了长期 运行,为了更好的代码稳定性,我想弄清楚为什么当我将 UserManager 投射到我的 sub[=106= 时没有引发回调].

有什么想法吗?我知道这是不可能的,但这可能是起订量中的错误吗?

--编辑 3--

实际上,它看起来是 VB 中的错误。它在 C# 中运行良好。

这是一个重现解决方案:OneDrive

raised an issue at the Moq repo 提供了一些有关如何使用和调试重现以获得相同结果的详细信息。

由于这个新发现,我将 VB.NET 和 C# 标签都添加到这个问题中。

谁能看出这两个项目之间的区别?

欢迎所有建议。

问题原来是 VB.NET 编译器中的一些草率,Moq 没有预料到必须考虑到这一点。

https://github.com/moq/moq4/issues/1067#issuecomment-706671833

已相应调整,将在即将发布的版本中提供。

非常感谢 stakx 在此方面出色而迅速的工作。