Search code examples
c#asynchronoustestingxamarinicommand

Async testing scenario fails when call made to command interface


I have a problem with testing an ICommand scenario in a Xamarin project. I have extracted the logic into a demonstration below.

Scenario N1 runs smoothly however I need Scenario N2 to work. The problem with scenario N2 is that as soon as it gets to the

await Task.Run(() => Task.Delay(1000)); 

it jumps back to the test method Assert where obviously the SetSurveyContext(int x) is not executed yet.

The strangest thing is that if I run this code from the Xamarin framework inside the app everything works fine probably because I am executing the Command in a wrong manner.

Really stuck with this question, I have tried numerous ways to run the command but neither have worked. Please help if someone has come across the same problem. Thanks.

Scenario 1 - working

[Test]
public async Task NewSurvey_SendObjectWithOnlyDate_StaticSurveyResourceIdAndDateSet()
{
        var mvmTest = new TesterPage();
        await mvmTest.NewSurvey();
        Assert.That(mvmTest.setter, Is.EqualTo(3));
}

public partial class TesterPage : ContentPage
{
    public int setter = 0;

    public TesterPage()
    {
        InitializeComponent ();
    }

    public async Task NewSurvey()
    {
        await PostNewSurvey();
    }

    private async Task PostNewSurvey()
    {
        var response = await Another();
        SetSurveyContext(response);
    }

    private async Task<int> Another()
    {
       await Task.Run(() => Task.Delay(1000));
       return 3;
    }

    private void SetSurveyContext(int x)
    {
        setter = x;
    }
}

Test green, everything runs smoothly.

Scenario N2 - fails

[Test]
public async Task NewSurvey_SendObjectWithOnlyDate_StaticSurveyResourceIdAndDateSet()
{
        var mvmTest = new TesterPage();
        mvmTest.NewSurveyCommand.Execute(null);
        Assert.That(mvmTest.setter, Is.EqualTo(3));
}

public partial class TesterPage : ContentPage
{
    public int setter = 0;

    public TesterPage ()
    {
        InitializeComponent ();
        NewSurveyCommand = new Command(async () => await NewSurvey());
    }

    public ICommand NewSurveyCommand { get; private set; }

    public async Task NewSurvey()
    {
        await PostNewSurvey();
    }

    private async Task PostNewSurvey()
    {
        var response = await Another();
        SetSurveyContext(response);
    }

    private async Task<int> Another()
    {
       await Task.Run(() => Task.Delay(1000));
       return 3;
    }

    private void SetSurveyContext(int x)
    {
        setter = x;
    }
}

Solution

  • Because I have solved this problem with @YuriZolotarev in chat, here the solution we found for everyone else who encounters it:
    The problem:
    When Task.Run() is called in TesterPage.Another() the main thread jumps back to the test method. There it executes Assert.That() immediately, even before SetSurveyContext has set setter to3.
    The solution:
    We found the solution to create a new private field in TesterPage which should contain the Task started in TesterPage.Another(). This Task can be waited for in the test method via Reflection. Everything could look like this:

    [Test]
    public async Task NewSurvey_SendObjectWithOnlyDate_StaticSurveyResourceIdAndDateSet()
    {
        var mvmTest = new TesterPage();
        mvmTest.NewSurveyCommand.Execute(null);
        // Use Reflection to wait for the task
        (GetInstanceField(typeof(TesterPage), mvmTest, "runningTask") as Task).Wait();
        Assert.That(mvmTest.setter, Is.EqualTo(3));
    }
    
    // A helper method to simplify Reflection
    internal static object GetInstanceField(Type type, object instance, string fieldName)
    {
        BindingFlags bindFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic
        | BindingFlags.Static;
        FieldInfo field = type.GetField(fieldName, bindFlags);
        return field.GetValue(instance);
    }
    
    public partial class TesterPage : ContentPage
    {
        public int setter = 0;
        private Task runningTask; // The field our Task object is saved in
    
        public TesterPage ()
        {
            InitializeComponent ();
            NewSurveyCommand = new Command(async () => await (runningTask = NewSurvey()));
        }
    
        public ICommand NewSurveyCommand { get; private set; }
    
        public async Task NewSurvey()
        {
            await PostNewSurvey();
        }
    
        private async Task PostNewSurvey()
        {
            var response = await Another();
            SetSurveyContext(response);
        }
    
        private async Task<int> Another()
        {
            await Task.Run(() => Task.Delay(1000));
            return 3;
        }
    
        private void SetSurveyContext(int x)
        {
            setter = x;
        }
    }