通过接口的 C# MVC 模拟模型破坏了控制器
C# MVC Mock Model through interface brokes the controller
我在控制器中有这个方法:
[HttpPatch]
[AllowAdminOnly]
public JsonResult EditUser(User _user)
{
try
{
if (ModelState.IsValid)
{
_user.Edit();
}
else
{
string error_messages = "";
foreach (var e in ModelState.Select(x => x.Value.Errors).Where(y => y.Count > 0).ToList())
{
error_messages += e[0].ErrorMessage + "\n";
}
throw new Exception(error_messages);
}
return MessageHelper(@Resources.Global.success, @Resources.Users.success_editing_user, "success");
}
catch (Exception err)
{
return ErrorHelper(@Resources.Global.error, @Resources.Users.error_editing_user, err.Message);
}
}
为了对该方法进行单元测试,我发现参考资料 (...) 说我应该 "mock" _user.Edit();
。看起来不错,它会避免将数据保存到数据库并使测试更快。
然后我的测试(对于有效用户)变为:
[TestMethod]
public void UserController_EditUser_Valid()
{
// Arrange
FundController controller = new UserController();
controller.ControllerContext = TestModelHelper.AdminControllerContext();
var _user = new Mock<Fund>();
_user.SetupGet(f => f.id).Returns(1);
_user.SetupGet(f => f.name).Returns("User name");
_user.SetupGet(f => f.nickname).Returns("User nickname");
_user.SetupGet(f => f.active).Returns(true);
_user.Setup(f => f.Edit()).Callback(() => {}).Verifiable();
// Act
var result = (JsonResult)controller.EditUser(_user.Object);
SimpleMessage resultMessage = m_serializer.Deserialize<SimpleMessage>(m_serializer.Serialize(result.Data));
// Assert
Assert.IsNotNull(resultMessage, "Result must not be null");
Assert.IsTrue(resultMessage.status.Equals("success"), "status must be 'success'");
}
但是当我这样做时,出现以下错误:
Test Name: UserController_EditUser_Valid
Test FullName: Pmanager.Tests.Controllers.UserControllerTest.UserController_EditUser_Valid
Test Source: ...\Pmanager\Pmanager.Tests\Controllers\UserControllerTest.cs : line 95
Test Outcome: Failed
Test Duration: 0:00:00,0179908
Result StackTrace:
em Moq.Mock.ThrowIfCantOverride(Expression setup, MethodInfo method)
em Moq.Mock.<>c__DisplayClass66_0`2.<SetupGet>b__0()
em Moq.PexProtector.Invoke[T](Func`1 function)
em Moq.Mock.SetupGet[T,TProperty](Mock`1 mock, Expression`1 expression, Condition condition)
em Moq.Mock`1.SetupGet[TProperty](Expression`1 expression)
em Pmanager.Tests.Controllers.UserControllerTest.UserController_EditUser_Valid() na ...\Pmanager\Pmanager.Tests\Controllers\UserControllerTest.cs:linha 100
Result Message:
Test method Pmanager.Tests.Controllers.UserControllerTest.UserController_EditUser_Valid threw exception:
System.NotSupportedException: Valid setup on a non-virtual (overridable in VB) member: f => f.id
我发现了一些关于 (...) 的文档说在创建 "mock" 时我应该使用接口而不是 class。
所以,我创建了一个界面:
public interface IUser
{
int id { get; set; }
string name { get; set; }
string nickname { get; set; }
bool active { get; set; }
void Edit();
}
然后我更改了 IUser
的所有内容,控制器的方法签名:
public JsonResult EditUser(IUser _user);
测试"mock"声明:
var _user = new Mock<IUser>();
等等。
现在测试可以了,但是实际控制者编辑用户的方法不行!
如何在不破坏控制器的真正功能的情况下将所有这些东西放在一起?
嗯,我仍然怀疑做这一切的正确方法。
但我想出了一种让两者都起作用的方法。它是:
- 保持控制器的方法签名应该是这样的:
public JsonResult EditUser(User _user);
- 将模拟传递给控制器的方法时在模拟中进行解析:
var result = (JsonResult)controller.EditUser(_user.Object as User);
虽然有效,但我不确定整个东西是否和谐...... :(
希望有人能给出更好的答案。
更新控制器以使用依赖关系更新用户模型。从模型本身中删除该功能。模型应该是 POCOs/DTOs。
public class UserController : Controller {
readonly IUserService userService;
public UserController(IUSerService userService) {
this.userService = userService;
}
[HttpPatch]
[AllowAdminOnly]
public JsonResult EditUser(User _user) {
try {
if (ModelState.IsValid) {
userService.Edit(user);
} else {
string error_messages = "";
foreach (var e in ModelState.Select(x => x.Value.Errors).Where(y => y.Count > 0).ToList()) {
error_messages += e[0].ErrorMessage + "\n";
}
throw new Exception(error_messages);
}
return MessageHelper(@Resources.Global.success, @Resources.Users.success_editing_user, "success");
} catch (Exception err) {
return ErrorHelper(@Resources.Global.error, @Resources.Users.error_editing_user, err.Message);
}
}
}
其中 IUserService
类似于
public interface IUserService {
void Edit(User user);
}
及其生产实施将执行所需的操作。请记住在您使用的任何 DI 中注册抽象和实现。
然后,测试会将其所需的依赖项模拟为 运行 隔离。
[TestMethod]
public void UserController_EditUser_Should_Be_Valid() {
// Arrange
var _user = new User {
id = 1,
name = "User name",
nickname = "User nickname",
active = true
};
var mockService = new Mock<IUserService>();
mockService .Setup(m => m.Edit(_user)).Verifiable();
var controller = new UserController(mockService.Object);
controller.ControllerContext = TestModelHelper.AdminControllerContext();
// Act
var result = controller.EditUser(_user) as JsonResult;
// Assert
Assert.IsNotNull(result, "Result must not be null");
mockService.Verify(); // verify that the service was call successfully.
}
我在控制器中有这个方法:
[HttpPatch]
[AllowAdminOnly]
public JsonResult EditUser(User _user)
{
try
{
if (ModelState.IsValid)
{
_user.Edit();
}
else
{
string error_messages = "";
foreach (var e in ModelState.Select(x => x.Value.Errors).Where(y => y.Count > 0).ToList())
{
error_messages += e[0].ErrorMessage + "\n";
}
throw new Exception(error_messages);
}
return MessageHelper(@Resources.Global.success, @Resources.Users.success_editing_user, "success");
}
catch (Exception err)
{
return ErrorHelper(@Resources.Global.error, @Resources.Users.error_editing_user, err.Message);
}
}
为了对该方法进行单元测试,我发现参考资料 (...) 说我应该 "mock" _user.Edit();
。看起来不错,它会避免将数据保存到数据库并使测试更快。
然后我的测试(对于有效用户)变为:
[TestMethod]
public void UserController_EditUser_Valid()
{
// Arrange
FundController controller = new UserController();
controller.ControllerContext = TestModelHelper.AdminControllerContext();
var _user = new Mock<Fund>();
_user.SetupGet(f => f.id).Returns(1);
_user.SetupGet(f => f.name).Returns("User name");
_user.SetupGet(f => f.nickname).Returns("User nickname");
_user.SetupGet(f => f.active).Returns(true);
_user.Setup(f => f.Edit()).Callback(() => {}).Verifiable();
// Act
var result = (JsonResult)controller.EditUser(_user.Object);
SimpleMessage resultMessage = m_serializer.Deserialize<SimpleMessage>(m_serializer.Serialize(result.Data));
// Assert
Assert.IsNotNull(resultMessage, "Result must not be null");
Assert.IsTrue(resultMessage.status.Equals("success"), "status must be 'success'");
}
但是当我这样做时,出现以下错误:
Test Name: UserController_EditUser_Valid
Test FullName: Pmanager.Tests.Controllers.UserControllerTest.UserController_EditUser_Valid
Test Source: ...\Pmanager\Pmanager.Tests\Controllers\UserControllerTest.cs : line 95
Test Outcome: Failed
Test Duration: 0:00:00,0179908
Result StackTrace:
em Moq.Mock.ThrowIfCantOverride(Expression setup, MethodInfo method)
em Moq.Mock.<>c__DisplayClass66_0`2.<SetupGet>b__0()
em Moq.PexProtector.Invoke[T](Func`1 function)
em Moq.Mock.SetupGet[T,TProperty](Mock`1 mock, Expression`1 expression, Condition condition)
em Moq.Mock`1.SetupGet[TProperty](Expression`1 expression)
em Pmanager.Tests.Controllers.UserControllerTest.UserController_EditUser_Valid() na ...\Pmanager\Pmanager.Tests\Controllers\UserControllerTest.cs:linha 100
Result Message:
Test method Pmanager.Tests.Controllers.UserControllerTest.UserController_EditUser_Valid threw exception:
System.NotSupportedException: Valid setup on a non-virtual (overridable in VB) member: f => f.id
我发现了一些关于 (...) 的文档说在创建 "mock" 时我应该使用接口而不是 class。
所以,我创建了一个界面:
public interface IUser
{
int id { get; set; }
string name { get; set; }
string nickname { get; set; }
bool active { get; set; }
void Edit();
}
然后我更改了 IUser
的所有内容,控制器的方法签名:
public JsonResult EditUser(IUser _user);
测试"mock"声明:
var _user = new Mock<IUser>();
等等。
现在测试可以了,但是实际控制者编辑用户的方法不行!
如何在不破坏控制器的真正功能的情况下将所有这些东西放在一起?
嗯,我仍然怀疑做这一切的正确方法。
但我想出了一种让两者都起作用的方法。它是:
- 保持控制器的方法签名应该是这样的:
public JsonResult EditUser(User _user);
- 将模拟传递给控制器的方法时在模拟中进行解析:
var result = (JsonResult)controller.EditUser(_user.Object as User);
虽然有效,但我不确定整个东西是否和谐...... :(
希望有人能给出更好的答案。
更新控制器以使用依赖关系更新用户模型。从模型本身中删除该功能。模型应该是 POCOs/DTOs。
public class UserController : Controller {
readonly IUserService userService;
public UserController(IUSerService userService) {
this.userService = userService;
}
[HttpPatch]
[AllowAdminOnly]
public JsonResult EditUser(User _user) {
try {
if (ModelState.IsValid) {
userService.Edit(user);
} else {
string error_messages = "";
foreach (var e in ModelState.Select(x => x.Value.Errors).Where(y => y.Count > 0).ToList()) {
error_messages += e[0].ErrorMessage + "\n";
}
throw new Exception(error_messages);
}
return MessageHelper(@Resources.Global.success, @Resources.Users.success_editing_user, "success");
} catch (Exception err) {
return ErrorHelper(@Resources.Global.error, @Resources.Users.error_editing_user, err.Message);
}
}
}
其中 IUserService
类似于
public interface IUserService {
void Edit(User user);
}
及其生产实施将执行所需的操作。请记住在您使用的任何 DI 中注册抽象和实现。
然后,测试会将其所需的依赖项模拟为 运行 隔离。
[TestMethod]
public void UserController_EditUser_Should_Be_Valid() {
// Arrange
var _user = new User {
id = 1,
name = "User name",
nickname = "User nickname",
active = true
};
var mockService = new Mock<IUserService>();
mockService .Setup(m => m.Edit(_user)).Verifiable();
var controller = new UserController(mockService.Object);
controller.ControllerContext = TestModelHelper.AdminControllerContext();
// Act
var result = controller.EditUser(_user) as JsonResult;
// Assert
Assert.IsNotNull(result, "Result must not be null");
mockService.Verify(); // verify that the service was call successfully.
}