Tuesday, February 18, 2020

Complex .Net unit testing of WinForms BeginInvoke()

This post is about writing unit tests for Winforms when there is threading involved.

Requirement

The nuget package DotNet.Helpers contain an API ISynchronizeInvokeExtensions which has an extension method InvokeIfRequired(). That provides the feature to invoke code that can manipulate UI controls from a different thread than it was created.

When unit testing the API, the test code has to execute using a properly threaded test.

Expected solution

It could seem very simple as below. Create a control in the current thread and access it from other thread.

[TestMethod]
public void WhenCalledFromOtherThread_ShouldUseBeginInvoke()
{
            Control ctrl = new Form();
            ctrl.Enabled = false;
            int threadIdWhereControlCreated = Thread.CurrentThread.ManagedThreadId;
            int threadIdWhereControlModified = -1;
            Task task = Task.Run(() =>
              {
                threadIdWhereControlModified = Thread.CurrentThread.ManagedThreadId;
                  ctrl.InvokeIfRequired(() =>
                  {
                      ctrl.Enabled = true;
                      ctrl.Text = "from therad";
                  });
              });
            task.Wait();        Assert.IsTrue(threadIdWhereControlCreated != threadIdWhereControlModified && ctrl.Enabled);
}

But it didn't work. Meaning InvokeIfRequired() didn't call the BeginInvoke(). Hence didn't get 100% coverage.

Working solution

Below is the working version.

    [TestClass]
    public class ISynchronizeInvokeExtensions_InvokeIfRequired
    {
        [TestMethod]
        public void WhenCalledFromOtherThread_ShouldUseBeginInvoke()
        {
            bool finished = false;
            TestForm testForm = new TestForm();
            testForm.Show();
            testForm.Finish += (sender, args) =>
            {
                testForm.InvokeIfRequired(() =>
                {
                    finished = true;
                    testForm.Close();
                });
            };
            testForm.Text = "trigger";
            while (!finished)
            {
                Application.DoEvents();
                Thread.Yield();
            }
            Assert.IsTrue(finished);
        }
    }
    public partial class TestForm : Form
    {
        public event EventHandler Finish;
        public TestForm()
        {
            this.TextChanged += (sender, args) =>
            {
                Thread runner = new Thread(() =>
                {
                    if (Finish != null)
                        Finish(this, EventArgs.Empty);
                });
                runner.Start();
            };
        }
    }

As seen in the code, the form is opened using the form.Show(). If that line is not there, the BeginInvoke will not get called.

Note: It works in the local development machine using Visual Studio and AppVeyor during CI/CD process. If the CI/CD tool selected doesn't support showing a form the unit test will not work.

Sample

Working code can be found at
https://github.com/joymon/dotnet-helpers/blob/master/DotNet.Helpers.Tests/WinForms/ISynchronizeInvokeExtensions_InvokeIfRequired.cs


Happy unit testing...

No comments: