使用 Moq 和 EF6 进行单元测试

Unit testing with Moq and EF6

我已经为我的服务层构建了单元测试。我没有使用 Mock,因为我认为既然你是 adding/deleting/querying 一个数据库,为什么要查询 mock 因为结果可能不同,但这不是我要问的。

现在我正在使用 Moq 来测试我的网络 api 层。我认为这很好,好像我所有的测试都在服务层上通过了,模拟服务来测试网络就可以了 api.

我已经设法为我的 GetAsync 方法编写了一个测试,它工作正常,就像这样

这里是控制器:

public async Task<IHttpActionResult> GetAsync(long id)
{
    Content content = await _service.GetAsync(id);
    ContentModel model = Mapper.Map<ContentModel>(content);

    return Ok(model);
}

测试如下:

[TestMethod]
public void Content_GetAsync()
{
    // arrange
    var mockService = new Mock<IContentService>();
    mockService.Setup(x => x.GetAsync(4))
        .ReturnsAsync(new Content
        {
            Id = 4
        });

    // setup automapper
    AutoMapperConfig.RegisterMappings();

    // act
    var controller = new ContentController(mockService.Object);
    var actionResult = controller.GetAsync(4).Result;
    var contentResult = actionResult as OkNegotiatedContentResult<ContentModel>;

    // assert
    Assert.IsNotNull(contentResult);
    Assert.IsNotNull(contentResult.Content);
    Assert.AreEqual(4, contentResult.Content.Id);
}

我相信我写的是正确的,而且它似乎有效。现在我想测试我的 PostAsync 方法来添加一个项目。控制器看起来像这样:

public async Task<IHttpActionResult> PostAsync(ContentModel model)
    {
        Content content = Mapper.Map<Content>(model);

        await _service.AddAsync(content);

        return Created<ContentModel>(Request.RequestUri, Mapper.Map<ContentModel>(content));
    }

测试如下:

[TestMethod]
public void Content_PostAsync()
{
    var mockService = new Mock<IContentService>();
    mockService.Setup(e => e.AddAsync(new Content()))
        .ReturnsAsync(1);

    // setup automapper
    AutoMapperConfig.RegisterMappings();

    // act
    var controller = new ContentController(mockService.Object);
    var actionResult = controller.PostAsync(new ContentModel {
        Heading = "New Heading"
    }).Result;
    var contentResult = actionResult as CreatedAtRouteNegotiatedContentResult<ContentModel>;

    // assert
    Assert.IsNotNull(contentResult);
    Assert.IsNotNull(contentResult.Content);
    Assert.AreEqual("New Heading", contentResult.Content.Heading);
}

现在当我 运行 这个时,我得到一个错误:

null reference exception.  "Request" from the Request.RequestUri is null.

所以我更改了我的控制器并对此进行了测试,以尝试模拟它。

测试代码:

public Task<IHttpActionResult> PostAsync(ContentModel model)
{
    return PostAsync(model, Request);
}

/// Unit testable version of above.  Cannot be accessed by users              
[NonAction]
public async Task<IHttpActionResult> PostAsync(ContentModel model, System.Net.Http.HttpRequestMessage request)
{
    Content content = Mapper.Map<Content>(model);

    await _service.AddAsync(content);

    return Created<ContentModel>(request.RequestUri, Mapper.Map<ContentModel>(content));
}

控制器代码:

[TestMethod]
public void Content_PostAsync()
{
    // arrange
    var mockRequest = new Mock<System.Net.Http.HttpRequestMessage>();
    mockRequest.Setup(e => e.RequestUri)
        .Returns(new Uri("http://localhost/"));

    var mockService = new Mock<IContentService>();
    mockService.Setup(e => e.AddAsync(new Content()))
        .ReturnsAsync(1);

    // setup automapper
    AutoMapperConfig.RegisterMappings();

    // act
    var controller = new ContentController(mockService.Object);
    var actionResult = controller.PostAsync(new ContentModel {
        Heading = "New Heading"
    }, mockRequest.Object).Result;
    var contentResult = actionResult as CreatedAtRouteNegotiatedContentResult<ContentModel>;

    // assert
    Assert.IsNotNull(contentResult);
    Assert.IsNotNull(contentResult.Content);
    Assert.AreEqual("New Heading", contentResult.Content.Heading);
}

现在我收到一条错误消息:

Invalid setup on a non-virtual (overridable in VB) member: e => e.RequestUri

有人可以帮我解决这个问题吗?我确定我在所有测试中都正确使用了 Mock,但单元测试对我来说是新手,所以也许我只是没有做正确的事。

使用 Moq 你只能模拟 virtual/absrtact 成员。 RequestUri 不是 HttpRequestMessage 的虚拟成员,因此出现错误消息。

您应该能够直接新建一个 HttpRequestMessage 而无需模拟它并将其传入。

var request = System.Net.Http.HttpRequestMessage>();
request.RequestUri = new Uri("http://localhost/");

// act
var controller = new ContentController(mockService.Object);
var actionResult = controller.PostAsync(new ContentModel {
    Heading = "New Heading"
}, request).Result;

Ned 的回答是正确的。 Moq 是一个 constrained 模拟库,这意味着它会生成您在运行时模拟的 classes 的动态子classes。如果这些 subclasses 没有在 mocked class 中声明为 virtual,则它们不能覆盖方法。您可以在 the art of unit testing.

中找到有关受限和不受限模拟库的更多信息

这就是为什么使用 mockist 风格的单元测试的人更喜欢模拟接口而不是具体的 classes,因为生成的 mock subclasses 可以很容易地覆盖(或者更确切地说,实现) 接口上的方法。