Skip to content

Instantly share code, notes, and snippets.

@marcin-chwedczuk
Created June 8, 2018 19:33
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save marcin-chwedczuk/e806bc402c5528fd0d763835ed347b22 to your computer and use it in GitHub Desktop.
Save marcin-chwedczuk/e806bc402c5528fd0d763835ed347b22 to your computer and use it in GitHub Desktop.
Kiedy unikać async/await w wyrażeniach lambda

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ę.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment