Saturday, 11 January 2014

How to access parameters passed in to a mocked method using Moq

The problem

I was writing unit tests for a WPF application using the MVVM pattern supported by Caliburn.Micro. I had methods on the view-model that were automatically bound to buttons in the user interface using Caliburn.Micro’s convention-based bindings. Some of these methods required that a pop-up dialog box be displayed to the end user to confirm an action. The dialog could be submitted or cancelled.
For example, when the user clicked a ‘back’ button to navigate back to a previous screen I needed to check if any data had changed on the current screen. If it had, I needed to present the user with a dialog asking if they really wanted to go back when there was unsaved data. If the user submitted the dialog by clicking OK the application would go to the previous screen.
The code looked something like this:
public void Back()
{
    if (!IsChanged)
    {
        EventAggregator.Publish(new NavigationMessage(GetBackScreen()));
    }
    else
    {
        var heading = ResourceProvider.GetString("Dialog.Navigate.Back.Heading");
        var text = ResourceProvider.GetString("Dialog.Navigate.Back.Text");
        var confirmationDialogViewModel = new ConfirmationDialogViewModel(heading, text);

        WindowManager.ShowChromelessDialog(confirmationDialogViewModel);

        if (confirmationDialogViewModel.Submitted)
        {
            EventAggregator.Publish(new NavigationMessage(GetBackScreen()));
        }
    }
}

By way of explanation, navigation in the application was managed by publishing navigation messages. The Shell view-model subscribed to the navigation messages and swapped the screens as appropriate when it handled such a message. Messaging was accomplished using Caliburn.Micro’s EventAggregator. Dialogs were managed using a class derived from Caliburn.Micro’s WindowManager class.
So the question was, given the code above how could I write unit tests that could test for cases when _confirmationDialogViewModel.Submitted was either true or false? My intention was to use Moq to mock the WindowManager so I would have to be able to access the _confirmationDialogViewModel passed in to WindowManager.ShowChromelessDialog(_confirmationDialogViewModel) and to be able to set its Submitted property.
How could I do that?

The solution

NB: Before I go on I should point out that there is an alternative – and arguably preferable - solution to the one presented below. Rather than instantiating the ConfirmationDialogViewModel as shown above it should have been injected into the parent view-model as a dependency. By so doing I could have simply set the Submitted property after instantiating the ConfirmationDialogViewModel in the unit test. This is what I actually ended up doing but the problem as presented above sparked my curiosity to see if it was possible to get at parameters passed into mocked methods.
Of course you can, by using Moq’s support for callbacks. There are several approaches to this but I liked the following:
[Test]
public void Back_IfConfirmationDialogIsSubmitted_NavigationMessageIsPublished()
{
    // Arrange
    _viewModel.Data = "Data has changed";

    ConfirmationDialogViewModel dialog;

    _mockEventAggregator.Setup(x => x.Publish(It.Is<NavigationMessage>(m => m.ScreenToNavigateTo == Screen.Home)));
    _mockWindowManager.Setup(w => w.ShowChromelessDialog(It.IsAny<ConfirmationDialogViewModel>()))
                      .Callback((object rootModel) => ((ConfirmationDialogViewModel)rootModel).Submit());

    // Act
    _viewModel.Back();

    // Assert
    _mockEventAggregator.Verify(x => x.Publish(It.Is<NavigationMessage>(m => m.ScreenToNavigateTo == Screen.Home)), Times.Once);
}

In this case we are able to call the Submit() method on the dialog view-model automatically when the mocked ShowChromelessDialog method is called (see lines 10 and 11). This in turn caused the Submitted property to be set to true. Job done!
Incidentally, if you need to you can get a reference to the method parameter by doing something like this:
[Test]
public void Back_IfConfirmationDialogIsSubmitted_NavigationMessageIsPublished()
{
    // Arrange
    _viewModel.Data = "Data has changed";
    _mockEventAggregator.Setup(x => x.Publish(It.Is<NavigationMessage>(m => m.ScreenToNavigateTo == Screens.EnquiryList)));

    ConfirmationDialogViewModel dialog = null;
    _mockWindowManager.Setup(w => w.ShowChromelessDialog(It.IsAny<ConfirmationDialogViewModel>()))
                      .Callback((object rootModel) => dialog = (ConfirmationDialogViewModel)rootModel);

    // Act
    _viewModel.Back();

    // We can now get at the dialog variable 
    var heading = dialog.Heading;

    // ... snip ...
 
}

In the example above you could access the dialog variable once the mocked ShowChromelessDialog method has been called.