Unit Testing Modal Dialogs in MVVM and Silverlight 4
As feedback to my recent post on Modal Dialogs in MVVM and Silverlight 4 I have received comments that the solution is not easy to Unit Test so i decided to create small blog posts on this subject just to show how simple and clean it is to do this.
So for those who are too lazy to read my original modal dialog post here is a quick reminder:
- I presented a simple solution how to abstract modal dialogs and message boxes in Silverlight 4 and call them from your ViewModel and act according to the results returned from those modal dialogs.
- I used Silverlight’s ChildWindow control because it was perfect fit for that job.
- Important thing is to say that ChildWindow control was used as implementation (with its ViewModel off course) but was hidden by abstractions (like IModalWindow and IModalWindowService) so it can easily be changed by some other implementation and unit tested, mocked out etc.
So in this post i will use the same simple Demo Application for the previous modal dialogs post but I will add few Unit Tests using Silverlight Unit Testing Framework and Silverlight verstion of RhinoMocks.
Our Sample app is very simple and just shows a view with list of some User entities.
Each of users can be edited by clicking on EDIT button next to them – this shows a modal dialog with User editor, we can change details and save them by clicking on OK or CANCEL.
Also there is option to delete each User by clicking on the DEL button – Message Box is shown with OK/Cancel buttons – these are very common scenarios for every LOB application so that’s why i picked them.
I must admit that i improved my framework since the previous post – I changed the signature of the IModalDialogService a little because now in my framework i have a IViewInitializationService that wires up ViewModels with Views so all this is used in this posts just to show it off 🙂 (more on the IViewInitializationService and the new way how i wire up ViewModels and View in one of the next posts).
So first let’s see the changes made to the IModalDialogView that is the abstraction of the actual modal dialog to be shown:
public interface IModalWindowView : IView { bool? DialogResult { get; set; } event EventHandler Closed; void Show(); void Close(); }
So the real ChildWindow that will be our dialog only needs to be marked as implementor of IModalWindowView interface with no additional code needed since this interface contains only methods and properties that ChildWindow already implements:
public partial class EditUserModalDialogView : IModalWindowView { public EditUserModalDialogView() { InitializeComponent(); } private void OKButton_Click(object sender, RoutedEventArgs e) { this.DialogResult = true; } private void CancelButton_Click(object sender, RoutedEventArgs e) { this.DialogResult = false; } }
Also you could see that our IModalDialogView implements our old friend – IView:
public interface IView { object FindName(string name); object DataContext { get; set; } string Name { get; set; } event RoutedEventHandler Loaded; }
Again nothing to implement here – no code to add – because ChildWindow already implements all this since its inheriting the ContentContol that has all this.
So now lets see the MainPageViewModel for our MainPageView that will actually show the modal dialogs and message boxes:
public class MainPageViewModel : ViewModel { private readonly IModalDialogService modalDialogService; private readonly IMessageBoxService messageBoxService; private ObservableCollection<User> users = new ObservableCollection<User>(); public MainPageViewModel(IModalDialogService modalDialogService, IMessageBoxService messageBoxService) { this.modalDialogService = modalDialogService; this.messageBoxService = messageBoxService; this.Users = new ObservableCollection<User>(); for (int i = 1; i < 6; i++ ) { this.Users.Add(new User {Username = string.Format("Admin User {0}", new Random((int)DateTime.Now.Ticks).Next(10,100)), IsAdmin = true}); Thread.Sleep(100); } this.ShowUserCommand = new DelegateCommand<User>(userInstanceToEdit => { this.modalDialogService.ShowDialog<EditUserModalDialogViewModel> ( userInstanceToEdit, (dialog, returnedViewModelInstance) => { if (dialog.DialogResult.HasValue && dialog.DialogResult.Value) { var oldItem = this.Users.FirstOrDefault( u => u.Id == userInstanceToEdit.Id); var oldPos = this.Users.IndexOf(oldItem); if (oldPos > -1) { this.Users.RemoveAt(oldPos); this.Users.Insert(oldPos, returnedViewModelInstance.User); } } }); }); this.DeleteUserCommand = new DelegateCommand<User> (p => { var result = this.messageBoxService.Show(string.Format( "Are you sure you want to delete user {0} ???", p.Username), "Please Confirm", GenericMessageBoxButton.OkCancel); if (result == GenericMessageBoxResult.Ok) { this.Users.Remove(p); }}); } public ObservableCollection<User> Users { get { return users; } set { users = value; this.OnPropertyChanged("Users"); } } private ICommand showUserCommand; public ICommand ShowUserCommand { get { return showUserCommand; } set { showUserCommand = value; this.OnPropertyChanged("ShowUserCommand"); } } private ICommand deleteUserCommand; public ICommand DeleteUserCommand { get { return deleteUserCommand; } set { deleteUserCommand = value; this.OnPropertyChanged("DeleteUserCommand"); } } }
As you see ViewModel is simple class and it just has a list of Users to be shown by its View and important things to notice here are two commands for editing and deleting single user.
ShowUserCommand shows modal dialog for editing single user, and DeleteUserCommand just shows MessageBox for confirmation of deletion with OK and Cancel buttons.
So now comes the fun part, lets write unit tests for those commands:
First UnitTest checks that our ShowUserCommand shows the modal dialog for editing the user (using the IModalDialogService). We put expectation on our IModalDialogService mock and invoke our command on ViewModel and verify all expectations afterwards.
Here is the code:
[TestMethod] public void ShowUserCommand_WhenCalledWithSpecificUser_ShowsModalDialogByPassingThatUserAsInitParam() { var viewModel = GetInitializedAndMockedViewModel(); viewModel.Users = GenerateSampleUsers(); var userToSend = viewModel.Users[1]; modalDialogService.Expect( p => p.ShowDialog<EditUserModalDialogViewModel>(null, null)) .Constraints( Is.Same(userToSend), Is.TypeOf(typeof(Action<IModalWindowView, EditUserModalDialogViewModel>))) .Repeat.Once(); viewModel.ShowUserCommand.Execute(userToSend); modalDialogService.VerifyAllExpectations(); }
We first get our ViewModel with Rhino-mocked instances of IModalDialogService and IMessageBoxService and some test Users by calling this methods:
private static ObservableCollection<User> GenerateSampleUsers() { return new ObservableCollection<User>() { new User() {Username = "user 1"}, new User() {Username = "user 2"}, new User() {Username = "user 3"} }; } private MainPageViewModel GetInitializedAndMockedViewModel() { this.modalDialogService = MockRepository.GenerateMock<IModalDialogService>(); this.messageBoxService = MockRepository.GenerateMock<IMessageBoxService>(); return new MainPageViewModel(this.modalDialogService, this.messageBoxService); }
So that was easy or what?
Lets go to next Unit Test. Lets check that when we call ShowUserCommand with instance of User that when it shows dialog and dialog returns true as its DialogResult – that we replace the original User in ViewModel’s list of Users with the one we got from the modal dialog:
[TestMethod] public void ShowUserCommand_WhenCalledWithSpecificUserAndOnModalDialogResultIsTrue_ShouldReplaceTheOriginalUserInstanceWithOneReturnedFromModel() { var viewModel = GetInitializedAndMockedViewModel(); viewModel.Users = GenerateSampleUsers(); var userToSend = viewModel.Users[1]; var dialog = MockRepository.GenerateMock<IModalWindowView>(); dialog.Expect(p => p.DialogResult).Return(true); var mockViewModel = MockRepository.GenerateStub<EditUserModalDialogViewModel>(); mockViewModel.User = new User() {Username = "mocked"}; modalDialogService.Expect( p => p.ShowDialog<EditUserModalDialogViewModel>(null, null)) .Constraints( Is.Same(userToSend), Is.TypeOf(typeof (Action<IModalWindowView, EditUserModalDialogViewModel>))) .Repeat.Once().WhenCalled( m => { var action = (m.Arguments[1] as Action<IModalWindowView, EditUserModalDialogViewModel>); if (action != null) { action.Invoke(dialog, mockViewModel); } }); viewModel.ShowUserCommand.Execute(userToSend); Assert.AreEqual("mocked", viewModel.Users[1].Username); }
And here is the unit test that checks the DeleteUserCommand so that it actually deletes the user from the ViewModel’s list of users if OK button was clicked on the shown message box:
[TestMethod] public void DeleteUserCommand_WhenCalledWithSpecificUserAndCallToMessageBoxServiceReturnsOk_ShouldDeleteThatUser() { // arrange var viewModel = GetInitializedAndMockedViewModel(); viewModel.Users = GenerateSampleUsers(); var userToSend = viewModel.Users[1]; this.messageBoxService.Expect(p => p.Show(null, null, GenericMessageBoxButton.OkCancel)).Constraints( Is.Anything(), Is.Anything(), Is.Anything()).Return(GenericMessageBoxResult.Ok); // act viewModel.DeleteUserCommand.Execute(userToSend); // assert user is not there anymore Assert.IsFalse(viewModel.Users.Any( u => u.Username == userToSend.Username)); }
So it turned out that its quite simple to test Modal Dialogs as long as you have the proper abstractions in place 😉
You can open this page to see all the unit tests running in browser.
Those tests are actually running on the code from the sample demo app!
And as you see all the Unit Tests are green, and the world is safe again 😀
Here is the code for all the tests, that is – all 9 of them:
namespace UnitTestingModalDialogs.Tests { [TestClass] public class MainPageViewModelTests { private IModalDialogService modalDialogService; private IMessageBoxService messageBoxService; private static ObservableCollection<User> GenerateSampleUsers() { return new ObservableCollection<User>() { new User() {Username = "user 1"}, new User() {Username = "user 2"}, new User() {Username = "user 3"} }; } private MainPageViewModel GetInitializedAndMockedViewModel() { this.modalDialogService = MockRepository.GenerateMock<IModalDialogService>(); this.messageBoxService = MockRepository.GenerateMock<IMessageBoxService>(); return new MainPageViewModel(this.modalDialogService, this.messageBoxService); } [TestMethod] public void ShowUserCommand_WhenCalledWithSpecificUser_ShowsModalDialogByPassingThatUserAsInitParam() { var viewModel = GetInitializedAndMockedViewModel(); viewModel.Users = GenerateSampleUsers(); var userToSend = viewModel.Users[1]; modalDialogService.Expect( p => p.ShowDialog<EditUserModalDialogViewModel>(null, null)) .Constraints( Is.Same(userToSend), Is.TypeOf(typeof(Action<IModalWindowView, EditUserModalDialogViewModel>))) .Repeat.Once(); viewModel.ShowUserCommand.Execute(userToSend); modalDialogService.VerifyAllExpectations(); } [TestMethod] public void ShowUserCommand_WhenCalledWithSpecificUserAndOnModalDialogResultIsTrue_ShouldReplaceTheOriginalUserInstanceWithOneReturnedFromModel() { var viewModel = GetInitializedAndMockedViewModel(); viewModel.Users = GenerateSampleUsers(); var userToSend = viewModel.Users[1]; var dialog = MockRepository.GenerateMock<IModalWindowView>(); dialog.Expect(p => p.DialogResult).Return(true); var mockViewModel = MockRepository.GenerateStub<EditUserModalDialogViewModel>(); mockViewModel.User = new User() {Username = "mocked"}; modalDialogService.Expect( p => p.ShowDialog<EditUserModalDialogViewModel>(null, null)) .Constraints( Is.Same(userToSend), Is.TypeOf(typeof (Action<IModalWindowView, EditUserModalDialogViewModel>))) .Repeat.Once().WhenCalled( m => { var action = (m.Arguments[1] as Action<IModalWindowView, EditUserModalDialogViewModel>); if (action != null) { action.Invoke(dialog, mockViewModel); } }); viewModel.ShowUserCommand.Execute(userToSend); Assert.AreEqual("mocked", viewModel.Users[1].Username); } [TestMethod] public void ShowUserCommand_WhenCalledWithSpecificUserAndOnModalDialogResultIsFalse_ShouldNotReplaceTheItemInViewModelWithOneReceivedFromDialog() { var viewModel = GetInitializedAndMockedViewModel(); viewModel.Users = GenerateSampleUsers(); var userToSend = viewModel.Users[1]; var dialog = MockRepository.GenerateMock<IModalWindowView>(); dialog.Expect(p => p.DialogResult).Return(false); var mockViewModel = MockRepository.GenerateStub<EditUserModalDialogViewModel>(); mockViewModel.User = new User() { Username = "mocked" }; modalDialogService.Expect( p => p.ShowDialog<EditUserModalDialogViewModel>(null, null)) .Constraints( Is.Same(userToSend), Is.TypeOf(typeof(Action<IModalWindowView, EditUserModalDialogViewModel>))) .Repeat.Once().WhenCalled( m => { var action = (m.Arguments[1] as Action<IModalWindowView, EditUserModalDialogViewModel>); if (action != null) { action.Invoke(dialog, mockViewModel); } }); viewModel.ShowUserCommand.Execute(userToSend); Assert.AreNotEqual("mocked", viewModel.Users[1].Username); } [TestMethod] public void ShowUserCommand_WhenCalledWithSpecificUserAndOnModalDialogResultIsNull_ShouldNotReplaceTheItemInViewModelWithOneReceivedFromDialog() { var viewModel = GetInitializedAndMockedViewModel(); viewModel.Users = GenerateSampleUsers(); var userToSend = viewModel.Users[1]; var dialog = MockRepository.GenerateMock<IModalWindowView>(); dialog.Expect(p => p.DialogResult).Return(null); var mockViewModel = MockRepository.GenerateStub<EditUserModalDialogViewModel>(); mockViewModel.User = new User() { Username = "mocked" }; modalDialogService.Expect( p => p.ShowDialog<EditUserModalDialogViewModel>(null, null)) .Constraints( Is.Same(userToSend), Is.TypeOf(typeof(Action<IModalWindowView, EditUserModalDialogViewModel>))) .Repeat.Once().WhenCalled( m => { var action = (m.Arguments[1] as Action<IModalWindowView, EditUserModalDialogViewModel>); if (action != null) { action.Invoke(dialog, mockViewModel); } }); viewModel.ShowUserCommand.Execute(userToSend); Assert.AreNotEqual("mocked", viewModel.Users[1].Username); } [TestMethod] public void DeleteUserCommand_WhenCalledWithSpecificUser_ShouldCallMessageBoxServiceWithSomeTextThatContainsThatUsersUsername() { // arrange var viewModel = GetInitializedAndMockedViewModel(); viewModel.Users = GenerateSampleUsers(); var userToSend = viewModel.Users[1]; this.messageBoxService.Expect(p => p.Show(null, null, GenericMessageBoxButton.OkCancel)).Constraints( Is.Matching<string>(txt => txt.Contains(userToSend.Username)), Is.Anything(), Is.Equal(GenericMessageBoxButton.OkCancel)).Return(GenericMessageBoxResult.Ok); // act viewModel.DeleteUserCommand.Execute(userToSend); // assert this.messageBoxService.VerifyAllExpectations(); } [TestMethod] public void DeleteUserCommand_WhenCalledWithSpecificUser_ShouldCallMessageBoxServiceWithOkCancelParamForButtons() { // arrange var viewModel = GetInitializedAndMockedViewModel(); viewModel.Users = GenerateSampleUsers(); var userToSend = viewModel.Users[1]; this.messageBoxService.Expect(p => p.Show(null, null, GenericMessageBoxButton.OkCancel)).Constraints( Is.Anything(), Is.Anything(), Is.Equal(GenericMessageBoxButton.OkCancel)).Return(GenericMessageBoxResult.Ok); // act viewModel.DeleteUserCommand.Execute(userToSend); // assert this.messageBoxService.VerifyAllExpectations(); } [TestMethod] public void DeleteUserCommand_WhenCalledWithSpecificUserAndCallToMessageBoxServiceReturnsOk_ShouldDeleteThatUser() { // arrange var viewModel = GetInitializedAndMockedViewModel(); viewModel.Users = GenerateSampleUsers(); var userToSend = viewModel.Users[1]; this.messageBoxService.Expect(p => p.Show(null, null, GenericMessageBoxButton.OkCancel)).Constraints( Is.Anything(), Is.Anything(), Is.Anything()).Return(GenericMessageBoxResult.Ok); // act viewModel.DeleteUserCommand.Execute(userToSend); // assert user is not there anymore Assert.IsFalse(viewModel.Users.Any( u => u.Username == userToSend.Username)); } [TestMethod] public void DeleteUserCommand_WhenCalledWithSpecificUserAndCallToMessageBoxServiceReturnsCancel_ShouldNOTDeleteThatUser() { // arrange var viewModel = GetInitializedAndMockedViewModel(); viewModel.Users = GenerateSampleUsers(); var userToSend = viewModel.Users[1]; this.messageBoxService.Expect(p => p.Show(null, null, GenericMessageBoxButton.OkCancel)).Constraints( Is.Anything(), Is.Anything(), Is.Anything()).Return(GenericMessageBoxResult.Cancel); // act viewModel.DeleteUserCommand.Execute(userToSend); // assert user is not there anymore Assert.IsTrue(viewModel.Users.Any(u => u.Username == userToSend.Username)); } [TestMethod] public void DeleteUserCommand_WhenCalledWithSpecificUserAndCallToMessageBoxServiceReturnsCancel_ShouldNOTDeleteAnyUser() { // arrange var viewModel = GetInitializedAndMockedViewModel(); viewModel.Users = GenerateSampleUsers(); var userToSend = viewModel.Users[1]; this.messageBoxService.Expect(p => p.Show(null, null, GenericMessageBoxButton.OkCancel)).Constraints( Is.Anything(), Is.Anything(), Is.Anything()).Return(GenericMessageBoxResult.Cancel); var usersCountBefore = viewModel.Users.Count; // act viewModel.DeleteUserCommand.Execute(userToSend); // assert user is not there anymore Assert.AreEqual(usersCountBefore, viewModel.Users.Count); } } }
If you want to investigate further download the Visual Studio 2010 solution and play with it – feel free to use the code as you wish and remember to have fun!