关于c#:在单元测试中模拟IHttpContextAccessor

Mock IHttpContextAccessor in Unit Tests

我有一种使用IHttpContextAccessor获取标头值的方法

1
2
3
4
5
6
7
8
9
10
11
12
public class HeaderConfiguration : IHeaderConfiguration
{
    public HeaderConfiguration()
    {

    }

    public string GetTenantId(IHttpContextAccessor httpContextAccessor)
    {
        return httpContextAccessor.HttpContext.Request.Headers["Tenant-ID"].ToString();
    }
}

我正在测试GetBookByBookId方法

假设方法看起来像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Book
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    private IHeaderConfiguration _headerConfiguration;
    private string _tenantID;

    public Book(IHeaderConfiguration headerConfiguration, IHttpContextAccessor httpContextAccessor){
        var headerConfig = new HeaderConfiguration();
        _httpContextAccessor = httpContextAccessor;
        _tenantID = headerConfig.GetTenantId(_httpContextAccessor);
    }

    public Task<List<BookModel>> GetBookByBookId(string id){
        //do something with the _tenantId
        //...
    }
}

这是我对GetBookByBookId方法的单元测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[Fact]
public void test_GetBookByBookId()
{
    //Arrange

    //Mock IHttpContextAccessor
    var mockHttpContextAccessor = new Mock<IHttpContextAccessor>();

    mockHttpContextAccessor.Setup(req => req.HttpContext.Request.Headers["Tenant-ID"].ToString()).Returns(It.IsAny<string>());
    //Mock HeaderConfiguration
    var mockHeaderConfiguration = new Mock<IHeaderConfiguration>();
    mockHeaderConfiguration.Setup(x => x.GetTenantId(mockHttpContextAccessor.Object)).Returns(It.IsAny<string>());

    var book = new Book( mockHttpContextAccessor.Object, mockHeaderConfiguration.Object);

    var bookId ="100";

    //Act
    var result = book.GetBookByBookId(bookId);

    //Assert
    result.Result.Should().NotBeNull().And.
        BeOfType<List<BookModel>>();
}

但是对于这一行:

1
mockHttpContextAccessor.Setup(req => req.HttpContext.Request.Headers["Tenant-ID"].ToString()).Returns(It.IsAny<string>());

它说

System.NotSupportedException: 'Type to mock must be an interface or an abstract or non-sealed class. '

我想知道使用标头值模拟IHttpContextAccessor的正确方法是什么?


您可以将DefaultHttpContext用作IHttpContextAccessor.HttpContext的后盾。 省去您设置太多事情的麻烦

接下来,您不能将It.IsAny()用作Returns结果。 它们只能在设置表达式中使用。

检查重构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
[Fact]
public async Task test_GetBookByBookId() {
    //Arrange

    //Mock IHttpContextAccessor
    var mockHttpContextAccessor = new Mock<IHttpContextAccessor>();
    var context = new DefaultHttpContext();
    var fakeTenantId ="abcd";
    context.Request.Headers["Tenant-ID"] = fakeTenantId;
    mockHttpContextAccessor.Setup(_ => _.HttpContext).Returns(context);
    //Mock HeaderConfiguration
    var mockHeaderConfiguration = new Mock<IHeaderConfiguration>();
    mockHeaderConfiguration
        .Setup(_ => _.GetTenantId(It.IsAny<IHttpContextAccessor>()))
        .Returns(fakeTenantId);

    var book = new Book(mockHttpContextAccessor.Object, mockHeaderConfiguration.Object);

    var bookId ="100";

    //Act
    var result = await book.GetBookByBookId(bookId);

    //Assert
    result.Should().NotBeNull().And.
        BeOfType<List<BookModel>>();
}

由于实际上应该显式注入HeaderConfiguration时,它正在手动初始化HeaderConfiguration,所以被测类也可能存在问题。

1
2
3
4
public Book(IHeaderConfiguration headerConfiguration, IHttpContextAccessor httpContextAccessor) {
    _httpContextAccessor = httpContextAccessor;
    _tenantID = headerConfiguration.GetTenantId(_httpContextAccessor);
}


在我的场景中,我不得不模拟IHttpContextAccessor并访问内部请求url位。
我在这里分享是因为我花了很多时间弄清楚这个问题,希望对您有所帮助。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
readonly Mock<IHttpContextAccessor> _HttpContextAccessor =
  new Mock<IHttpContextAccessor>(MockBehavior.Strict);

void SetupHttpContextAccessorWithUrl(string currentUrl)
{
  var httpContext = new DefaultHttpContext();
  setRequestUrl(httpContext.Request, currentUrl);

  _HttpContextAccessor
    .SetupGet(accessor => accessor.HttpContext)
    .Returns(httpContext);

  static void setRequestUrl(HttpRequest httpRequest, string url)
  {
    UriHelper
      .FromAbsolute(url, out var scheme, out var host, out var path, out var query,
        fragment: out var _);

    httpRequest.Scheme = scheme;
    httpRequest.Host = host;
    httpRequest.Path = path;
    httpRequest.QueryString = query;
  }
}