简体   繁体   English

C# MVC Mock Model 通过接口破解控制器

[英]C# MVC Mock Model through interface brokes the controller

I have this method inside a 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);
    }
}

In order to unit test this method, I have found references (...) saying that I should "mock" the _user.Edit();为了对该方法进行单元测试,我发现参考文献 (...) 说我应该“模拟” _user.Edit(); . . It seems fine, it will avoid saving data to database and will make the tests faster.看起来不错,它将避免将数据保存到数据库并使测试更快。

My test then (for a valid user) becomes:我的测试然后(对于有效用户)变为:

[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'");
}

But when I do this, I get the following error:但是当我这样做时,我收到以下错误:

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

I found some documentation about (...) saying that I should use an interface instead of the class when creating the "mock" .我发现了一些关于 (...) 的文档,说我应该在创建"mock"时使用接口而不是类。

So, I created an interface:所以,我创建了一个界面:

public interface IUser
{
    int id { get; set; }
    string name { get; set; }
    string nickname { get; set; }
    bool active { get; set; }
    void Edit();
}

and then I changed everything for IUser , the controller's method signature:然后我改变了IUser所有内容,控制器的方法签名:

public JsonResult EditUser(IUser _user);

the test "mock" declaration:测试“模拟”声明:

var _user = new Mock<IUser>();

and so on.等等。

Now the test works, but the actual controller's method for editing the user doesn't!现在测试成功了,但实际控制器编辑用户的方法没有!

How can I put all these things together without breaking the controller trully functionallity?如何在不破坏控制器真正功能的情况下将所有这些东西放在一起?

Update controller to use a dependency to update the user model.更新控制器以使用依赖项来更新用户模型。 Remove that functionality from the model itself.从模型本身中删除该功能。 Models should be POCOs/DTOs.模型应该是 POCO/DTO。

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);
        }
    }
}

where the IUserService is something like IUserService是这样的

public interface IUserService {
    void Edit(User user);
}

and its production implementation would perform the desired action.并且其生产实施将执行所需的操作。 Remember to register abstraction and implementation with what ever DI you are using.请记住使用您正在使用的任何 DI 注册抽象和实现。

The test would then mock the dependencies needed for it to run in isolation.然后,测试将模拟其独立运行所需的依赖项。

[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. 
}

Well, I am still in doubt about the correct way of doing all this.好吧,我仍然怀疑做这一切的正确方法。

But I figured a way of making both working.但我想出了一种让两者都工作的方法。 It is:这是:

  • keep the controller's method signature as it should be: public JsonResult EditUser(User _user);保持控制器的方法签名,因为它应该是: public JsonResult EditUser(User _user);
  • do a parse in the mock when passing it to the controller's method: var result = (JsonResult)controller.EditUser(_user.Object as User);在将其传递给控制器​​的方法时在模拟中进行解析: var result = (JsonResult)controller.EditUser(_user.Object as User);

Although it works, I am not sure whether the whole stuff is in harmony... :(虽然它有效,但我不确定整个事情是否和谐...... :(

Hope someone could give a better answer.希望有人能给出更好的答案。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM