测试内部使用 SemaphoneSlim 实现并行化的异步方法

Testing an async method that uses SemaphoneSlim internally to achieve parallelisation

我为 IEnumerable<Uri> 编写了一个扩展方法,以允许下载 URI 中指定的资源。这是简化的代码:

public static async Task DownloadInParallel(this IEnumerable<Uri> values, HttpClient httpClient, Func<Uri, int, Stream, Task> successCallback, int maxDownloadsInParallel, CancellationToken cancellationToken)
{
    var throttler = new SemaphoreSlim(initialCount: maxDownloadsInParallel);

    var tasks = values.Select(async (x, i) =>
    {
        await throttler.WaitAsync(cancellationToken).ConfigureAwait(false);

        try
        {
            using (var response = await httpClient.GetAsync(x, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false))
            using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
            {
                await successCallback(x, i, stream).ConfigureAwait(false);
            }
        }
        finally
        {
            throttler.Release();
        }
    });

    await Task.WhenAll(tasks).ConfigureAwait(false);
}

使用 mock HttpMessageHandler 编写单元测试来断言成功回调的执行是直接的,但在我看来这还不够好,因为扩展方法提供的基本功能不仅仅是资源列表已下载,但并行执行这些下载的能力达到指定限制。然而,这很难测试,因为无法看到线程何时进入信号量。这意味着我不能做与并行相关的断言,它有两个方面:下载确实是并行执行的而不是顺序执行的,并且并行度达到但没有超过指定的限制。

作为一种变通方法,我考虑向包含的 class 添加事件,在输入和释放信号量时将调用这些事件。通过预处理器指令使用条件编译并将它们设置为内部并使用 InternalsVisibleToAttribute.

,这些不会在发布版本中公开
#if TEST
internal static event EventHandler SemaphoreEnter;
internal static event EventHandler SemaphoreRelease;
#endif

public static async Task DownloadInParallel(this IEnumerable<Uri> values, HttpClient httpClient, Func<Uri, int, Stream, Task> successCallback, int maxDownloadsInParallel, CancellationToken cancellationToken)
{
    var throttler = new SemaphoreSlim(initialCount: maxDownloadsInParallel);

    var tasks = values.Select(async (x, i) =>
    {
        await throttler.WaitAsync(cancellationToken).ConfigureAwait(false);
#if TEST
        SemaphoreEnter?.Invoke(null, EventArgs.Empty);
#endif
        try
        {
            ...
        }
        finally
        {
            throttler.Release();
#if TEST
            SemaphoreRelease?.Invoke(null, EventArgs.Empty);
#endif
        }
    });

    await Task.WhenAll(tasks).ConfigureAwait(false);
}

这使我可以构建一系列被调用的事件,以便我可以进行断言。虽然它有效,但感觉像是一个 hacky 解决方案。这合理吗?我可以使用哪些其他方法来测试此核心功能?还有其他更适合的线程结构吗?

我太专注于让这个与线程特定组件一起工作,我忽略了简单的答案。

我的测试结果是:

HttpHandlerMock.Protected()
               .Setup<Task<HttpResponseMessage>>(...)
               .ReturnsAsync(...);

采纳了 Paolo 的评论并通过添加 Callback 方法将代码更改为以下内容,这使我能够对 Events 集合执行所需的断言。

HttpHandlerMock.Protected()
               .Setup<Task<HttpResponseMessage>>(...)
               .Callback(() => Events.Add("start"))
               .ReturnsAsync(...);