Od pewnego czasu do pisania testów jednostkowych używam bibliotek XUnit, NSubstitute oraz NFluent. Zwłaszcza ta ostatnia przypadła mi do gustu.
Jedną z metod oferowanych przez NFluent jest Check.ThatAsyncCode(...)
, na przykład:
Check
.ThatAsyncCode(() => Task.Delay(0))
.DoesNotThrow();
Niestety zauważyłem że stosowanie tej metody przysparza niektórym osobom sporo trudności.
Aby lepiej zilustrować problem przyjrzyjmy się kodowi prostego komponentu:
public interface IUserNameProvider {
Task<string> GetUserNameAsync();
}
public class QuoteUserNameDecorator : IUserNameProvider {
private readonly IUserNameProvider _inner;
public async Task<string> GetUserNameAsync() {
var userName = await _inner.GetUserNameAsync();
return string.Format("'{0}'", userName);
}
}
I testowi który został dla niego stworzony:
public class QuoteUserNameDecoratorTests
{
private readonly IUserNameProvider _userNameProvider;
private readonly QuoteUserNameDecorator _decorator;
public QuoteUserNameDecoratorTests()
{
// This is how you create mocks using NSubstitute
_userNameProvider = Substitute.For<IDummyDependency>();
_decorator = new QuoteUserNameDecorator(_userNameProvider);
}
[Fact]
public void Should_return_quoted_empty_string_given_null_user_name()
{
// Arrange
// NSubstitute - setup mock
_userNameProvider
.GetUserNameAsync()
.Returns((string)null);
// Assert
Check
.ThatAsyncCode(async() => await _decorator.GetUserNameAsync())
.DoesNotThrow().And
.WhichResult().IsEqualTo("''");
}
}
Kod testu działa poprawnie, ale jest nieoptymalny. Winna jest temu linijka:
.ThatAsyncCode(async() => await _decorator.GetUserNameAsync())
którą z powodzeniem można zastąpić przez:
.ThatAsyncCode(() => _decorator.GetUserNameAsync())
Działanie programu będzie w obu przypadkach identyczne. Można powiedzieć
że usuwając async/await
z wyrażenia lambda usuwamy pośrednika w wywołaniu
docelowej metody _decorator.GetUserNameAsync
.
Szczegółowe wyjaśnienie dlaczego powyższa transformacja działa wcale nie jest proste (wymaga przeanalizowania kodu generowanego przez kompilator dla wyrażenia lambda z async/await
). Dociekliwych zachęcam do porównania kodu generowanego przez kompilator dla programu:
using System;
using System.Threading.Tasks;
class Example
{
static Task Main(string[] args)
{
// return RunAsyncCode(async() => await DummyAsyncOperation());
return RunAsyncCode(() => DummyAsyncOperation());
}
public static async Task RunAsyncCode(Func<Task> asyncCode)
{
Console.WriteLine("This is RunAsyncCode method.");
await asyncCode();
}
public static async Task DummyAsyncOperation()
{
Console.WriteLine("This is DummyAsyncOperation method.");
await Task.Delay(TimeSpan.FromSeconds(5));
}
}
Dla przypadku z i bez async/await
. Najlepiej do tego wykorzystać stronę https://sharplab.io/ (dawniej TryRoslyn) z opcją "Results: C#". Dzięki temu dostaniemy kod w C# 2.0 a nie w IL'u, co znacznie ułatwi analizę.