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

问题描述

我正在建立一个多租户网站,因此我需要重载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.City)

因此,无论我做什么,oIdentity都是nothing(在C#中为null)。

我尝试在打开CallBase的情况下创建模拟,就像这样:

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

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

任何人都可以发现为何未调用此回调的原因吗?

-编辑1-

此后,我发现不再为模拟的任何方法引发回调,而不仅仅是重载方法。就像过去那样,这非常奇怪。

我已确定要从备份中还原代码,并将其返回到工作状态。我将尽快获得更多信息。

-编辑2-

问题仍然没有解决,但是我已经接近了。

当我将模拟的UserManager转换为Identity框架类(即UserManager(Of TUser))时,回调将成功引发。但是,当我将其强制转换为子类时,为了获取重载的方法,它会失败。

这就是为什么我最初给人的印象是只有重载方法才导致失败-因为我必须更改转换才能模拟该方法。更换铸件是问题的根源,而不是过载。

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

这是我完整的UserManager子类:

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,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转换为子类时没有引发回调。

有什么想法吗?我知道这是一个漫长的过程,但这可能是Moq中的错误吗?

-编辑3-

实际上,这似乎是VB中的错误。在C#中可以正常工作。

这是一个复制解决方案:OneDrive

我已经raised an issue at the Moq repo,其中提供了一些有关如何使用和调试repro以获得相同结果的详细信息。

由于这一新发现,我将VB.NET和C#标记添加到此问题中。

谁能发现两个项目之间的差异?

欢迎提出所有建议。

解决方法

问题原来是Moq没想到必须考虑的VB.NET编译器的草率。

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

已相应调整,将在以后的版本中提供。

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