没有为重载的模拟方法引发回调
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
都是 Nothing
(null
在 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
在此方面出色而迅速的工作。
我正在构建一个多租户网站,因此我需要重载 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
都是 Nothing
(null
在 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
在此方面出色而迅速的工作。