首页 > 解决方案 > How to mock AngleSharp's HttpRequester to provide static content in unit tests?

问题描述

I am working on an application that includes a web scraper for gathering data and would like to verify that a particular service (which in the future will grow to several services) is performing the correct business logic given an expected HTML DOM tree is returned to it.

Rather than performing actual HTTP requests each time a test is run, I would prefer to "mock" this out by providing a static document for the test and returning a pre-defined HTML document. I'd prefer instead for my unit test to reflect "Given this HTML document, verify the output is correct business wise" which does not need to include AngleSharp's HTTP request.

Here's how I load the document now, which I have placed into a "wrapper" service that I can then inject into my service through dependency injection:

var angleSharpConfig = Configuration.Default.WithDefaultLoader();
var angleSharpContext = BrowsingContext.New(angleSharpConfig);
var document = await angleSharpContext.OpenAsync(url);

I see that there is a LoaderOptions object that can be passed to WithDefaultLoader, but it does not seem to provide a way to mock the HTTP request. There does seem to possibly be a way to do this with the With method on the default configuration object, however I am struggling to see how to sanely do this.

Other suggestions for alternate approaches are welcome, as well - due to my inexperience I may be attempting to skin a cat that needn't be skinned at all.

标签: moqxunit.netanglesharp

解决方案


呵呵,男孩,这是一个笨蛋吗?老实说,当钢锯在这里做得足够好时,我可能仍在使用电锯,所以我仍然欢迎在这里对我的“解决方案”提出建设性的反馈。

为了让 AngleSharp 能够“做它的事情”并将我IDocument保存到磁盘的文件的静态返回给我,我必须做一些事情(其中一些是我在写这个问题时开始的,其他的我想出了艰辛的道路):

  1. 将对 AngleSharp 的调用包装在一个服务中,然后 DI 可以注入(我可以模拟)
  2. 使所说的服务调用带一个参数,通常只是在 null 时IRequester传递null和使用new DefaultHttpRequester()
  3. 创建我自己的IRequester, 但 make的实现RequestAsyncSupportsProtocol virtual以便 Moq 可以做它的事情。最简单的方法(在 VS Code 中)是右键单击与您的假类名在同一行的接口,并让 IDE 自动生成实现。每个方法都会抛出一个异常,但它会履行合同。
  4. 在调用被测单元之前做一个两阶段的舞蹈:
    • 创建您的 AngleSharp 包装器类的实例(真正的实现,而不是接口或模拟),然后创建一个最小的模拟IRequester,模拟地址、标题和内容(从磁盘读取 HTML 文件)并将其传递给包装器方法,取回磁盘上完整合法IDocument的 HTML 文件。
    • 创建一个您的 AngleSharp 包装类的模拟并模拟该方法,告诉它IDocument从上一步返回,然后为您的测试单元提供它。

嘎。这太疯狂了,我现在明白为什么这个问题是 Google 上的第一个结果:)

无论如何,对于细节,这是我想出的解决这个问题的代码的粗略草图:

测试:

[Fact]
public async void ItShouldDoSomething() {
    using(var autoMock = AutoMock.GetLoose()) {
        var mockedWrapper = autoMock.Mock<IAngleSharpWrapper>();
        var fakeDocument = await GetFakeDocument();
        mockedWrapper.Setup(x => x.OpenUrlAsync(It.IsAny<IRequester>())).ReturnsAsync(fakeDocument);
        autoMock.Provide(mockedWrapper);

        var sut = autoMock.Create<ClassYouAreTesting>();

        var data = await sut.LoadData();

        //Perform your assertions here
    }
}

相关的存根和模拟方法:

private async Task<IDocument> GetFakeDocument() {
    var angleSharpWrapper = new AngleSharpWrapper();
    var requesterMock = GetFakeRequesterMock();
    return await angleSharpWrapper.OpenUrlAsync("http://askjdkaj", requesterMock.Object);
}

private Mock<FakeRequester> GetFakeRequesterMock() {
    var mockResponse = new Mock<IResponse>();
    mockResponse.Setup(x => x.Address).Returns(new Url("fakeaddress"));
    mockResponse.Setup(x => x.Headers).Returns(new Dictionary<string, string>());
    mockResponse.Setup(_ => _.Content).Returns(LoadFakeDocumentFromFile());
    var mockFakeRequester = new Mock<FakeRequester>();

    mockFakeRequester.Setup(_ => _.RequestAsync(It.IsAny<Request>(), It.IsAny<CancellationToken>())).ReturnsAsync(mockResponse.Object);
    mockFakeRequester.Setup(x => x.SupportsProtocol(It.IsAny<string>())).Returns(true);
    return mockFakeRequester;
}

private MemoryStream LoadFakeDocumentFromFile() {
    //Note: path below is relative to your output directory, so bin/Debug/etc.
    using (FileStream fileStream = File.OpenRead(Path.Combine(Directory.GetCurrentDirectory(), "Path/To/Your/LocalFile.html")))
    {
        MemoryStream memoryStream = new MemoryStream();
        memoryStream.SetLength(fileStream.Length);
        fileStream.Read(memoryStream.GetBuffer(), 0, (int)fileStream.Length);

        return memoryStream;
    }
}

*编辑:我想我也应该包括 AngleSharp 的包装方法,这很重要:

public async Task<IDocument> OpenUrlAsync(string url, IRequester defaultRequester)
{
    if (defaultRequester == null) {
        defaultRequester = new DefaultHttpRequester();
    }

    var angleSharpConfig = Configuration.Default.WithDefaultLoader().With(defaultRequester);
    var angleSharpContext = BrowsingContext.New(angleSharpConfig);
    return await angleSharpContext.OpenAsync(url);
}

推荐阅读