web-dev-qa-db-fra.com

La méthode asynchrone bloque avec TestScheduler dans ReactiveUI

J'essaie d'utiliser l'ordonnanceur de test reactiveui avec une méthode asynchrone dans un test.

Le test est suspendu lorsque l'appel asynchrone est attendu.

La cause fondamentale semble être une commande attendue dans la méthode asynchrone.

    [Fact]
    public async Task Test()
        => await new TestScheduler().With(async scheduler =>
        {
            await SomeAsyncMethod();

            // *** execution never gets here
            Debugger.Break();
        });

    private async Task SomeAsyncMethod()
    {
        var command = ReactiveCommand.CreateFromTask(async () =>
        {
            await Task.Delay(100);
        });

        // *** this hangs
        await command.Execute();
    }

Comment puis-je effectuer un appel asynchrone en combinaison avec le planificateur de test sans blocage?

J'utilise reactiveui 9.4.1

MODIFIER:

J'ai essayé la méthode WithAsync() comme suggéré dans la réponse Funks, mais le comportement est le même.

12
thumbmunkeys

Comment puis-je effectuer un appel asynchrone en combinaison avec le planificateur de test?

En bref

command.Execute() est une observable froide. Vous devez vous y abonner au lieu d'utiliser await.

Étant donné votre intérêt pour TestScheduler, je suppose que vous voulez tester quelque chose qui demande du temps. Cependant, à partir de Quand devrais-je me préoccuper de la planification section:

les threads créés via "new Thread ()" ou "Task.Run" ne peuvent pas être contrôlés dans un test unitaire.

Donc, si vous voulez vérifier, par exemple, si votre Task se termine dans un délai de 100 ms, vous devrez attendre que la méthode asynchrone se termine. Pour être sûr, c'est pas le type de test TestScheduler est destiné.

La version un peu plus longue

TestScheduler a pour but de vérifier les flux de travaux en mettant les choses en mouvement et en vérifiant l'état à certains moments. Comme nous ne pouvons manipuler le temps que sur une TestScheduler, vous préférerez généralement ne pas attendre que le code async réel soit complet, étant donné qu'il est impossible de faire avancer rapidement les calculs ou les E/S réels. N'oubliez pas qu'il s'agit de vérifier les flux de travail: vm.A a une nouvelle valeur à 20 ms, donc vm.B devrait avoir un nouveau val à 120 ms, ...

Alors, comment pouvez-vous tester le SUT?

1\Vous pouvez vous moquer de la méthode asynchrone en utilisant scheduler.CreateColdObservable

public class ViewModelTests
{
    [Fact]
    public void Test()
    {
        string observed = "";

        new TestScheduler().With(scheduler =>
        {
            var observable = scheduler.CreateColdObservable(
                scheduler.OnNextAt(100, "Done"));

            observable.Subscribe(value => observed = value);
            Assert.Equal("", observed);

            scheduler.AdvanceByMs(99);
            Assert.Equal("", observed);

            scheduler.AdvanceByMs(1);
            Assert.Equal("Done", observed);
        });
    }
}

Ici, nous avons essentiellement remplacé command.Execute() par var observable créé le scheduler.

Il est clair que l'exemple ci-dessus est plutôt simple, mais avec plusieurs observables se notifiant, ce type de test peut fournir des informations précieuses, ainsi qu'un filet de sécurité lors de la refactorisation.

Ref:

2\Vous pouvez référencer explicitement la IScheduler

a) Utilisation des planificateurs fournis par RxApp

public class MyViewModel : ReactiveObject
{
    public string Observed { get; set; }

    public MyViewModel()
    {
        Observed = "";

        this.MyCommand = ReactiveCommand
            .CreateFromTask(SomeAsyncMethod);
    }

    public ReactiveCommand<Unit, Unit> MyCommand { get; }

    private async Task SomeAsyncMethod()
    {
        await RxApp.TaskpoolScheduler.Sleep(TimeSpan.FromMilliseconds(100));
        Observed = "Done";
    }
}

public class ViewModelTests
{
    [Fact]
    public void Test()
    {
        new TestScheduler().With(scheduler =>
        {
            var vm = new MyViewModel();

            vm.MyCommand.Execute().Subscribe();
            Assert.Equal("", vm.Observed);

            scheduler.AdvanceByMs(99);
            Assert.Equal("", vm.Observed);

            scheduler.AdvanceByMs(1);
            Assert.Equal("Done", vm.Observed);
        });
    }
}

Remarque

  • CreateFromTask crée une ReactiveCommand avec une logique d'exécution asynchrone. Il n'est pas nécessaire de définir la méthode Test comme asynchrone ni d'attendre la TestScheduler.

  • Dans l'étendue de la méthode d'extension WithRxApp.TaskpoolScheduler = RxApp.MainThreadScheduler = la new TestScheduler().

b) Gérer vos propres ordonnanceurs par injection de constructeur

public class MyViewModel : ReactiveObject
{
    private readonly IScheduler _taskpoolScheduler;
    public string Observed { get; set; }

    public MyViewModel(IScheduler scheduler)
    {
        _taskpoolScheduler = scheduler;
        Observed = "";

        this.MyCommand = ReactiveCommand
            .CreateFromTask(SomeAsyncMethod);
    }

    public ReactiveCommand<Unit, Unit> MyCommand { get; }

    private async Task SomeAsyncMethod()
    {
        await _taskpoolScheduler.Sleep(TimeSpan.FromMilliseconds(100));
        Observed = "Done";
    }
}

public class ViewModelTests
{
    [Fact]
    public void Test()
    {
        new TestScheduler().With(scheduler =>
        {
            var vm = new MyViewModel(scheduler); ;

            vm.MyCommand.Execute().Subscribe();
            Assert.Equal("", vm.Observed);

            scheduler.AdvanceByMs(99);
            Assert.Equal("", vm.Observed);

            scheduler.AdvanceByMs(0);
            Assert.Equal("Done", vm.Observed);
        });
    }
}

Ref:

Fermons les rangs avec une autre citation de Haacked:

Malheureusement, et ce point est important, la TestScheduler ne s’étendant pas dans la vie réelle, vos manigances sont donc limitées à votre code réactif asynchrone. Ainsi, si vous appelez Thread.Sleep(1000) dans votre test, ce thread sera réellement bloqué pendant une seconde. Mais en ce qui concerne le planificateur de test, aucun temps n’est passé.

3
Funk

Avez-vous essayé d'utiliser ConfigureAwait (false) lors de l'appel d'une méthode imbriquée?

 [Fact]
    public async Task Test()
        => await new TestScheduler().With(async scheduler =>
        {
            // this hangs
            await SomeAsyncMethod().ConfigureAwait(false);

            // ***** execution will never get to here
            Debugger.Break();
        }
1
MistyK

Essayez d’utiliser .ConfigureAwait(false) sur toutes vos méthodes asynchrones ..__ Cela vous donnera un comportement non bloquant.

[Fact]
public async Task Test()
    => await new TestScheduler().With(async scheduler =>
    {
        await SomeAsyncMethod().ConfigureAwait(false);

        // *** execution never gets here
        Debugger.Break();
    }).ConfigureAwait(false);

private async Task SomeAsyncMethod()
{
    var command = ReactiveCommand.CreateFromTask(async () =>
    {
        await Task.Delay(100).ConfigureAwait(false);
    }).ConfigureAwait(false);

    // *** this hangs
    await command.Execute();
}

Un autre moyen de vérifier si le problème est lié à ConfigureAwait consiste à porter votre projet sur Asp.Net Core et à le tester. 

Asp.net core n’a pas besoin d’utiliser ConfigureAwait pour éviter ce problème de blocage.

Cochez cette référence

0
Simonare