How to Unit Test BackgroundWorker + PRISM InteractionRequest?

I have a reoccurring pattern in my MVF WPF applications that has the following structure.

public class MyViewModel : NotificationObject
{
    private readonly IService _DoSomethingService;

    private bool _IsBusy;
    public bool IsBusy
    {
        get { return _IsBusy; }
        set
        {
            if (_IsBusy != value)
            (
                _IsBusy = value;
                RaisePropertyChanged(() => IsBusy);
            )
        }
    }

    public ICommand DisplayInputDialogCommand { get; private set; }
    public InteractionRequest<Notification> Error_InteractionRequest { get; private set; }
    public InteractionRequest<Confirmation> GetInput_InteractionRequest { get; private set; }

    // ctor
    public MyViewModel(IService service)
    {
        _DoSomethingService = service;

        DisplayInputDialogCommand  = new DelegateCommand(DisplayInputDialog);
        Error_InteractionRequest = new InteractionRequest<Notification>();
        Input_InteractionRequest = new InteractionRequest<Confirmation>();
    }

    private void DisplayInputDialog()
    {
        Input_InteractionRequest.Raise(
            new Confirmation() {
                Title = "Please provide input...",
                Content = new InputViewModel()
            },
            ProcessInput
        );
    }

    private void ProcessInput(Confirmation context)
    {
        if (context.Confirmed)
        {
            IsBusy = true;

            BackgroundWorker bg = new BackgroundWorker();
            bg.DoWork += new DoWorkEventHandler(DoSomethingWorker_DoWork);
            bg.RunWorkerCompleted += new RunWorkerCompletedEventHandler(DoSomethingWorker_RunWorkerCompleted);
            bg.RunWorkerAsync();
        }
    }

    private void DoSomethingWorker_DoWork(object sender, DoWorkEventArgs e)
    {
        _DoSomethingService.DoSomething();
    }

    private void DoSomethingWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        IsBusy = false;

        if (e.Error != null)
        {
            Error_InteractionRequest.Raise(
                new Confirmation() {
                    Title = "Error",
                    Content = e.Error.Message
                }
            );
        }
    }
}

Essentially, the template describes a dialog-oriented workflow that allows the user to initiate (and provide input) continuous operation without blocking the user interface. A specific example of this template could be the “Save As ...” operation, when the user clicks the “Save As ...” button, then the keys in the text value for the file name in the pop-up dialog box, then clicks the OK dialog button, then watches the rotation animation while their data is stored in the specified file name.

In the above code example, initiating this workflow will perform the following operations.

  • Input_InteractionRequest Raised, .

  • ProcessInput (, ).

  • Confirmed InteractionRequest, , .

  • ...

    • IsBusy.

    • a BackgroundWorker _DoSomethingService.DoSomething().

    • IsBusy.

    • DoSomething_DoWork , Error_InteractionRequest Raised, , .

, , . , . , .

  • BackgroundWorker IBackgroundWorker ctor. IBackgroundWorker, , DoWork/RunWorkerCompleted. InteractionRequest.

  • System.Threading.Thread.Sleep(int), BackgroundWorker . , , , InteractionRequest.

  • BackgroundWorker InteractionRequest Humble Objects, . , .

  • Unit test DoSomethingWorker_DoWork, DoSomethingWorker_RunWorkerCompleted ProcessInput . , , , .

unit test / , ?

+5
2

EDIT: . ( .NET 4.0+).

BackgroundWorker , , question. BackgroundWorker , InteractionRequest .

, .

public interface IDelegateWorker
{
    void Start<TInput, TResult>(Func<TInput, TResult> onStart, Action<TResult> onComplete, TInput parm);
}

Start, .

  • Func<TInput, TResult> onStart - BackgroundWorker.DoWork. . TInput TResult, onComplete.

  • Action<TResult> onComplete - BackgroundWorker.RunWorkerCompleted. onStart. . TResult.

  • TInput parm - onStart ( null, onStart ). Backgroundworker.RunWorkerAsync(object argument).

, BackgroundWorker IDelegateWorker. , MyViewModel .

public class MyViewModel : NotificationObject
{
    // Dependencies
    private readonly IService _doSomethingService;
    private readonly IDelegateWorker _delegateWorker; // new

    private bool _IsBusy;
    public bool IsBusy
    {
        get { return _IsBusy; }
        set
        {
            if (_IsBusy != value)
            {
                _IsBusy = value;
                RaisePropertyChanged(() => IsBusy);
            }
        }
    }

    public ICommand DisplayInputDialogCommand { get; private set; }
    public InteractionRequest<Notification> ErrorDialogInteractionRequest { get; private set; }
    public InteractionRequest<Confirmation> InputDialogInteractionRequest { get; private set; }

    // ctor
    public MyViewModel(IService service, IDelegateWorker delegateWorker /* new */)
    {
        _doSomethingService = service;
        _delegateWorker = delegateWorker; // new

        DisplayInputDialogCommand = new DelegateCommand(DisplayInputDialog);
        ErrorDialogInteractionRequest = new InteractionRequest<Notification>();
        InputDialogInteractionRequest = new InteractionRequest<Confirmation>();
    }

    private void DisplayInputDialog()
    {
        InputDialogInteractionRequest.Raise(
            new Confirmation()
            {
                Title = "Please provide input...",
                Content = new DialogContentViewModel()
            },
            ProcessInput
        );
    }

    private void ProcessInput(Confirmation context)
    {
        if (context.Confirmed)
        {
            IsBusy = true;

            // New - BackgroundWorker now abstracted behind IDelegateWorker interface.
            _delegateWorker.Start<object, TaskResult<object>>(
                    ProcessInput_onStart,
                    ProcessInput_onComplete,
                    null
                );
        }
    }

    private TaskResult<object> ProcessInput_onStart(object parm)
    {
        TaskResult<object> result = new TaskResult<object>();
        try
        {
            result.Result = _doSomethingService.DoSomething();
        }
        catch (Exception ex)
        {
            result.Error = ex;
        }
        return result;
    }

    private void ProcessInput_onComplete(TaskResult<object> tr)
    {
        IsBusy = false;

        if (tr.Error != null)
        {
            ErrorDialogInteractionRequest.Raise(
                new Confirmation()
                {
                    Title = "Error",
                    Content = tr.Error.Message
                }
            );
        }
    }

    // Helper Class
    public class TaskResult<T>
    {
        public Exception Error;
        public T Result;
    }
}

BackgroundWorker, ( ) IDelegateWorker MyViewModel . , .

public class DelegateWorker : IDelegateWorker
{
    public void Start<TInput, TResult>(Func<TInput, TResult> onStart, Action<TResult> onComplete, TInput parm)
    {
        TResult result = default(TResult);

        if (onStart != null)
            result = onStart(parm);

        if (onComplete != null)
            onComplete(result);
    }
}

.

public class ASyncDelegateWorker : IDelegateWorker
{
    public void Start<TInput, TResult>(Func<TInput, TResult> onStart, Action<TResult> onComplete, TInput parm)
    {
        BackgroundWorker bg = new BackgroundWorker();
        bg.DoWork += (s, e) =>
        {
            if (onStart != null)
                e.Result = onStart((TInput)e.Argument);
        };

        bg.RunWorkerCompleted += (s, e) =>
        {
            if (onComplete != null)
                onComplete((TResult)e.Result);
        };

        bg.RunWorkerAsync(parm);
    }
}

, InteractionRequest . , MSTest Moq 100% - Visual Cover Code Coverage, .

[TestClass()]
public class MyViewModelTest
{
    [TestMethod()]
    public void DisplayInputDialogCommand_OnExecute_ShowsDialog()
    {
        // Arrange
        Mock<IService> mockService = new Mock<IService>();
        Mock<IDelegateWorker> mockWorker = new Mock<IDelegateWorker>();
        MyViewModel vm = new MyViewModel(mockService.Object, mockWorker.Object);
        InteractionRequestTestHelper<Confirmation> irHelper
            = new InteractionRequestTestHelper<Confirmation>(vm.InputDialogInteractionRequest);

        // Act
        vm.DisplayInputDialogCommand.Execute(null);

        // Assert
        Assert.IsTrue(irHelper.RequestRaised);
    }

    [TestMethod()]
    public void DisplayInputDialogCommand_OnExecute_DialogHasCorrectTitle()
    {
        // Arrange
        const string INPUT_DIALOG_TITLE = "Please provide input...";
        Mock<IService> mockService = new Mock<IService>();
        Mock<IDelegateWorker> mockWorker = new Mock<IDelegateWorker>();
        MyViewModel vm = new MyViewModel(mockService.Object, mockWorker.Object);
        InteractionRequestTestHelper<Confirmation> irHelper
            = new InteractionRequestTestHelper<Confirmation>(vm.InputDialogInteractionRequest);

        // Act
        vm.DisplayInputDialogCommand.Execute(null);

        // Assert
        Assert.AreEqual(irHelper.Title, INPUT_DIALOG_TITLE);
    }

    [TestMethod()]
    public void DisplayInputDialogCommand_OnExecute_SetsIsBusyWhenDialogConfirmed()
    {
        // Arrange
        Mock<IService> mockService = new Mock<IService>();
        Mock<IDelegateWorker> mockWorker = new Mock<IDelegateWorker>();
        MyViewModel vm = new MyViewModel(mockService.Object, mockWorker.Object);
        vm.InputDialogInteractionRequest.Raised += (s, e) =>
        {
            Confirmation context = e.Context as Confirmation;
            context.Confirmed = true;
            e.Callback();
        };

        // Act
        vm.DisplayInputDialogCommand.Execute(null);

        // Assert
        Assert.IsTrue(vm.IsBusy);
    }

    [TestMethod()]
    public void DisplayInputDialogCommand_OnExecute_CallsDoSomethingWhenDialogConfirmed()
    {
        // Arrange
        Mock<IService> mockService = new Mock<IService>();
        IDelegateWorker worker = new DelegateWorker();
        MyViewModel vm = new MyViewModel(mockService.Object, worker);
        vm.InputDialogInteractionRequest.Raised += (s, e) =>
        {
            Confirmation context = e.Context as Confirmation;
            context.Confirmed = true;
            e.Callback();
        };

        // Act
        vm.DisplayInputDialogCommand.Execute(null);

        // Assert
        mockService.Verify(s => s.DoSomething(), Times.Once());
    }

    [TestMethod()]
    public void DisplayInputDialogCommand_OnExecute_ClearsIsBusyWhenDone()
    {
        // Arrange
        Mock<IService> mockService = new Mock<IService>();
        IDelegateWorker worker = new DelegateWorker();
        MyViewModel vm = new MyViewModel(mockService.Object, worker);
        vm.InputDialogInteractionRequest.Raised += (s, e) =>
        {
            Confirmation context = e.Context as Confirmation;
            context.Confirmed = true;
            e.Callback();
        };

        // Act
        vm.DisplayInputDialogCommand.Execute(null);

        // Assert
        Assert.IsFalse(vm.IsBusy);
    }

    [TestMethod()]
    public void DisplayInputDialogCommand_OnExecuteThrowsError_ShowsErrorDialog()
    {
        // Arrange
        Mock<IService> mockService = new Mock<IService>();
        mockService.Setup(s => s.DoSomething()).Throws(new Exception());
        DelegateWorker worker = new DelegateWorker();
        MyViewModel vm = new MyViewModel(mockService.Object, worker);
        vm.InputDialogInteractionRequest.Raised += (s, e) =>
        {
            Confirmation context = e.Context as Confirmation;
            context.Confirmed = true;
            e.Callback();
        };
        InteractionRequestTestHelper<Notification> irHelper
            = new InteractionRequestTestHelper<Notification>(vm.ErrorDialogInteractionRequest);

        // Act
        vm.DisplayInputDialogCommand.Execute(null);

        // Assert
        Assert.IsTrue(irHelper.RequestRaised);
    }

    [TestMethod()]
    public void DisplayInputDialogCommand_OnExecuteThrowsError_ShowsErrorDialogWithCorrectTitle()
    {
        // Arrange
        const string ERROR_TITLE = "Error";
        Mock<IService> mockService = new Mock<IService>();
        mockService.Setup(s => s.DoSomething()).Throws(new Exception());
        DelegateWorker worker = new DelegateWorker();
        MyViewModel vm = new MyViewModel(mockService.Object, worker);
        vm.InputDialogInteractionRequest.Raised += (s, e) =>
        {
            Confirmation context = e.Context as Confirmation;
            context.Confirmed = true;
            e.Callback();
        };
        InteractionRequestTestHelper<Notification> irHelper
            = new InteractionRequestTestHelper<Notification>(vm.ErrorDialogInteractionRequest);

        // Act
        vm.DisplayInputDialogCommand.Execute(null);

        // Assert
        Assert.AreEqual(irHelper.Title, ERROR_TITLE);
    }

    [TestMethod()]
    public void DisplayInputDialogCommand_OnExecuteThrowsError_ShowsErrorDialogWithCorrectErrorMessage()
    {
        // Arrange
        const string ERROR_MESSAGE_TEXT = "do something failed";
        Mock<IService> mockService = new Mock<IService>();
        mockService.Setup(s => s.DoSomething()).Throws(new Exception(ERROR_MESSAGE_TEXT));
        DelegateWorker worker = new DelegateWorker();
        MyViewModel vm = new MyViewModel(mockService.Object, worker);
        vm.InputDialogInteractionRequest.Raised += (s, e) =>
        {
            Confirmation context = e.Context as Confirmation;
            context.Confirmed = true;
            e.Callback();
        };
        InteractionRequestTestHelper<Notification> irHelper
            = new InteractionRequestTestHelper<Notification>(vm.ErrorDialogInteractionRequest);

        // Act
        vm.DisplayInputDialogCommand.Execute(null);

        // Assert
        Assert.AreEqual((string)irHelper.Content, ERROR_MESSAGE_TEXT);
    }

    // Helper Class
    public class InteractionRequestTestHelper<T> where T : Notification
    {
        public bool RequestRaised { get; private set; }
        public string Title { get; private set; }
        public object Content { get; private set; }

        public InteractionRequestTestHelper(InteractionRequest<T> request)
        {
            request.Raised += new EventHandler<InteractionRequestedEventArgs>(
                (s, e) =>
                {
                    RequestRaised = true;
                    Title = e.Context.Title;
                    Content = e.Context.Content;
                });
        }
    }
}

:

  • - TypeMock (mocking). , . TypeMock . , , , .

  • .NET 4.5 BackgroundWorker async/await. IDelegateWorker ( ), , async/await ViewModel.

:

, , .NET 4.0 . unit test - , , .

Microsoft (TPL) .NET 4.0. , BackgroundWorker. - TPL, Task . , , .

[TestMethod]
public void RunATest()
{
    // Assert.
    var sut = new MyClass();

    // Act.
    sut.DoSomethingAsync().Wait();

    // Assert.
    Assert.IsTrue(sut.SomethingHappened);
}

unit test, - . ThreadPool. , . , .

Task.Factory.StartNew(
    () => DoSomething(),
    TaskScheduler.FromCurrentSynchronizationContext());

, , Injection Dependency. - , , , ThreadPool.

, , . , , . , , . .

// Configure the default task scheduler to use the current synchronization context.
Type taskSchedulerType = typeof(TaskScheduler);
FieldInfo defaultTaskSchedulerField = taskSchedulerType.GetField("s_defaultTaskScheduler", BindingFlags.SetField | BindingFlags.Static | BindingFlags.NonPublic);
defaultTaskSchedulerField.SetValue(null, TaskScheduler.FromCurrentSynchronizationContext());

, , , unit test. , , , SynchronizationContext, .

Error: System.InvalidOperationException: The current SynchronizationContext may not be used as a TaskScheduler.

, SynchronizationContext .

// Configure the current synchronization context to process work synchronously.
SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());

, - . , SynchronizationContext ThreadPool. , SynchronizationContext Post .

public class TestSynchronizationContext : SynchronizationContext
{
    public override void Post(SendOrPostCallback d, object state)
    {
        Send(d, state);
    }
}

, .

// Configure the current synchronization context to process work synchronously.
SynchronizationContext.SetSynchronizationContext(new TestSynchronizationContext());

// Configure the default task scheduler to use the current synchronization context.
Type taskSchedulerType = typeof(TaskScheduler);
FieldInfo defaultTaskSchedulerField = taskSchedulerType.GetField("s_defaultTaskScheduler", BindingFlags.SetField | BindingFlags.Static | BindingFlags.NonPublic);
defaultTaskSchedulerField.SetValue(null, TaskScheduler.FromCurrentSynchronizationContext());

, . Injection Dependency Injection, .

+6

. 3 .

  • InteractionRequest , , . , . IoC ( )
  • DoWork RunWorker. , ( ).
  • IsAsyncFlag, , . Async.

. 100% - , . , .

, .

+1

All Articles