测试内部使用 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(...);
我为 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(...);