问题描述
我正在建立一个多租户网站,因此我需要重载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#中可以正常工作。
我已经raised an issue at the Moq repo,其中提供了一些有关如何使用和调试repro以获得相同结果的详细信息。
由于这一新发现,我将VB.NET和C#标记都添加到此问题中。
谁能发现两个项目之间的差异?
欢迎提出所有建议。
解决方法
问题原来是Moq没想到必须考虑的VB.NET编译器的草率。
https://github.com/moq/moq4/issues/1067#issuecomment-706671833
已相应调整,将在以后的版本中提供。
非常感谢stakx
在此方面的出色而迅速的工作。