我正在构建一个多租户网站,因此我需要重载UserManager.CreateAsync()
以接受类型为City
的附加参数。换句话说,每个User
都属于一个City
。我已经相应地在User
模型上配置了额外的属性。
问题是,我无法让Moq为重载的mock方法引发回调。
这是我的模拟设置:
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
都是Nothing
(C#中的null
(。
我试着在CallBase
打开的情况下创建mock,就像这样:
oManagerMock = New Mock(Of UserManager)(oStoreMock.Object) With {.CallBase = True}
但这导致重载的方法本身被运行,这当然不行。我运行的是单元测试,而不是集成测试。
有人能发现为什么没有调用这个回调吗?
--编辑1-
从那以后,我发现不再为我模拟的任何方法引发回调,而不仅仅是重载方法。这是非常奇怪的,因为它在过去是有效的。
我决定从备份中恢复代码,并将其恢复到工作状态。我很快就会有更多的信息。
-编辑2-
这个问题还没有解决,但我已经接近了。
当我将mock的UserManager
转换为Identity框架类(即UserManager(Of TUser)
(时,成功地引发了回调。然而,当我将它强制转换到我的子类时,为了获得重载的方法,它失败了。
这就是为什么我最初认为只有重载的方法才会导致失败的原因——因为我必须更改类型转换才能模拟该方法。造成问题的不是过载,而是铸件的更换。
我已经检查了Identity框架的源代码,没有发现任何与我的概念不同的地方。
这是我完整的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, 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
转换到我的子类时没有引发回调。
有什么想法吗?我知道,这是一个长镜头,但这可能是Moq的一个bug吗?
-编辑3-
实际上,它看起来是VB中的一个bug。它在C#中运行良好。
这里有一个修复解决方案:OneDrive
我在Moq repo中提出了一个问题,在那里我提供了一些关于如何使用和调试repo以获得相同结果的细节。
由于这一新发现,我将为这个问题添加VB.NET和C#标记。
有人能发现这两个项目之间的区别吗?
欢迎所有建议。
问题原来是VB.NET编译器中的一些草率,Moq没有预料到这一点。
https://github.com/moq/moq4/issues/1067#issuecomment-706671833
它已经进行了相应的调整,将在即将发布的版本中提供。
非常感谢stakx
在这方面出色而迅速的工作。