Windows Phone 7, MVVM and TDD (Part 3 – LoginCommand)
by Peter Daukintis
Now, to support the login functionality I will add a password property in exactly the same way (test first) as the username text property from part 2 (http://babaandthepigman.spaces.live.com/blog/cns!4F1B7368284539E5!237.entry). Also I will supplement the tests with tests to check the values are set, similar to the following:
[TestMethod] public void UsernameText_SetValue_ResultsInNewValue() { var mainViewModel = new MainViewModel(); mainViewModel.UsernameText = "fred"; Assert.AreEqual("fred", mainViewModel.UsernameText); }
Now I have all the required user input properties, I will turn my attention towards how the login command will get invoked. Following the MVVM style we would like to expose some implementation code to be available to be data-bound to the user interface. Now in WPF, this can be achieved via the ICommand interface which can be bound to the ui. However, Windows Phone is based upon Silverlight 3 which has no support for the ICommand interface. There are a number of ways to hook this functionality in yourself but I will use one way provided for me by the MVVM Light framework. First I will implement the ICommand supporting code then we will see how to wire it all up.
So, first create a new ICommand property for the login command:
public ICommand LoginCommand { get; set; }
And, in the MainViewModel constructor instantiate it with a RelayCommand:
LoginCommand = new RelayCommand(ExecuteLogin, CanExecuteLogin);
supplied by the MVVM Light framework. (For further information on the RelayCommand see http://babaandthepigman.spaces.live.com/blog/cns!4F1B7368284539E5!167.entry and http://msdn.microsoft.com/en-us/magazine/dd419663.aspx).
Basically the ExecuteLogin and CanExecuteLogin are delegates which clients can supply to be called when the ICommand interface is called (ICommand has two methods CanExecute and Execute).
These two methods declared in the MainViewModel keep everything compiling:
private bool CanExecuteLogin() { return true; } private void ExecuteLogin() { }
Now, I have enough to begin writing tests for the login command.
I can start my first test off with something like this:
[TestMethod] public void LoginCommand_ExecutedWithValidLogin_ResultsInSuccessfulLogin() { var mainViewModel = new MainViewModel(); mainViewModel.LoginCommand.Execute(null); }
But, I am now forced to think about how my login will be provided and also what state it will act upon, since currently I have no state or behaviour to assert on in my tests. Ultimately, the login functionality will be provided by my model layer so to enable a clear separation of concerns I will use an IUserRepository interface to provide the functionality (see http://babaandthepigman.spaces.live.com/blog/cns!4F1B7368284539E5!202.entry for a discussion on why this would be a good idea). The IUserRepository is defined as follows:
public interface IUserRepository { bool Login(string username, string password); }
and our login function in our view model can call it and not really worry about how or where it is implemented.
So, our first naive implementation of the ExecuteLogin call will just call IUserRepository::Login. But first it needs a reference to one and to keep everything nice and flexible we will allow one to be passed in to the constructor of the view model.
So, pass it in and initialise a private field.
public MainViewModel(IUserRepository repository) { Repository = repository;
Then, our ExecuteLogin call can become:
private void ExecuteLogin() { IsLoggedIn = Repository.Login(UsernameText, PasswordText); }
Where IsLoggedIn is a boolean property. Back to our unit tests which will need refactoring since they will no longer compile. In order to get them compiling we need to supply a reference to an IUserRepository when we construct the view model. So we will create a Fake user repository like this:
public class FakeUserRepository : IUserRepository { public Func<string, string, bool> LoginAction { get; set; } #region Implementation of IUserRepository public bool Login(string username, string password) { if (LoginAction != null) return LoginAction(username, password); return true; } #endregion }
This allows our test to modify the logic inside the Login call by providing a delegate which may be useful moving forwards. So, now in our tests each call to construct the view model can be replaced by:
var fakeUserRepository = new FakeUserRepository(); var mainViewModel = new MainViewModel(fakeUserRepository);
and the test method we were working on can become this:
[TestMethod] public void LoginCommand_ExecutedWithValidLogin_ResultsInSuccessfulLogin() { var fakeUserRepository = new FakeUserRepository(); var mainViewModel = new MainViewModel(fakeUserRepository); mainViewModel.UsernameText = "fred"; mainViewModel.PasswordText = "fred"; mainViewModel.LoginCommand.Execute(null); Assert.IsTrue(mainViewModel.IsLoggedIn); }
Now, we need to think a bit as the ExecuteLogin call is going to block the UI thread whilst it executes which is clearly no good in our Silverlight environment. So, if we make the login call asynchronous how will we be able to unit test it? Well, the Silverlight Unit Test framework supports this and this is how it’s done:
Firstly, my ExecuteLogin method becomes:
private void ExecuteLogin() { var backgroundWorker = new BackgroundWorker(); backgroundWorker.DoWork += BackgroundWorkerDoWork; backgroundWorker.RunWorkerCompleted += BackgroundWorkerRunWorkerCompleted; backgroundWorker.RunWorkerAsync(new Userdata { Username = UsernameText, Password = PasswordText }); } void BackgroundWorkerDoWork(object sender, DoWorkEventArgs e) { var argument = e.Argument as Userdata; if (argument == null) { e.Result = false; return; } var password = argument.Password; var username = argument.Username; e.Result = Repository.Login(username, password); } private void BackgroundWorkerRunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { var result = (bool)e.Result; IsLoggedIn = result; if (IsLoggedIn == false) { UsernameText = string.Empty; PasswordText = string.Empty; } else { // navigate to another page... //VisualStateManager.GoToState() } }
With an additional helper class to pass around the user credentials:
public class Userdata { public string Username { get; set; } public string Password { get; set; } }
Now if I run my unit test the Login method will return immediately and the background worker will run on – we need to wait until the background worker finishes before we can assert on IsLoggedIn. The unit test becomes:
[TestMethod] [Asynchronous] public void LoginCommand_ExecutedWithValidLogin_ResultsInSuccessfulLogin() { var fakeUserRepository = new FakeUserRepository(); fakeUserRepository.LoginAction = (u, p) => true; var mainViewModel = new MainViewModel(fakeUserRepository) { UsernameText = "fred", PasswordText = "fred" }; mainViewModel.LoginCommand.Execute(null); EnqueueConditional(() => mainViewModel.IsLoading == false); EnqueueCallback(() => Assert.IsTrue(mainViewModel.IsLoggedIn)); EnqueueTestComplete(); }
First note the Asynchronous attribute added to the test method – this denotes that the test will run some async code. Also, notice the Enqueue… methods: The EnqueueConditional call will block until the condition calculates to true, so in our case when IsLoading is false (IsLoading is a boolean property set to true whilst loading is in progress, otherwise false) i.e. the loading is complete. Then we can correctly assert the IsLoggedIn property. For further details of the Enqueue methods see http://www.jeff.wilcox.name/2009/03/asynchronous-testing/.
Sprinkle on a few more tests (test invalid login and CanExecute);
[TestMethod] [Asynchronous] public void LoginCommand_ExecutedWithInValidLogin_ResultsInFailedLogin() { var fakeUserRepository = new FakeUserRepository(); fakeUserRepository.LoginAction = (u, p) => false; var mainViewModel = new MainViewModel(fakeUserRepository); mainViewModel.LoginCommand.Execute(null); EnqueueConditional(() => mainViewModel.IsLoading == false); EnqueueCallback(() => Assert.IsFalse(mainViewModel.IsLoggedIn)); EnqueueTestComplete(); } [TestMethod] public void LoginCommand_CanExecuteReturnsTrue_GivenValidUsernameAndPassword() { var fakeUserRepository = new FakeUserRepository(); var mainViewModel = new MainViewModel(fakeUserRepository) { UsernameText = "user", PasswordText = "pwd" }; bool canExecute = mainViewModel.LoginCommand.CanExecute(null); Assert.IsTrue(canExecute); }
Results in the following ‘green’ tests…hmmm…satisfying….
Technorati Tags: MVVM,Part,LoginCommand,password,text,spaces,Also,TestMethod,UsernameText_SetValue_ResultsInNewValue,MainViewModel,UsernameText,Assert,AreEqual,user,attention,expose,implementation,code,data,interface,ICommand,hook,framework,wire,RelayCommand,ExecuteLogin,CanExecuteLogin,magazine,clients,CanExecute,Execute,LoginCommand_ExecutedWithValidLogin_ResultsInSuccessfulLogin,behaviour,layer,separation,IUserRepository,discussion,Login,needs,reference,repository,IsLoggedIn,PasswordText,Where,Back,unit,Fake,FakeUserRepository,Func,LoginAction,region,logic,method,IsTrue,environment,Test,supports,BackgroundWorker,DoWork,BackgroundWorkerDoWork,RunWorkerAsync,Userdata,Username,sender,DoWorkEventArgs,argument,Result,RunWorkerCompletedEventArgs,VisualStateManager,GoToState,helper,credentials,background,worker,Asynchronous,EnqueueConditional,EnqueueCallback,EnqueueTestComplete,Enqueue,Sprinkle,LoginCommand_ExecutedWithInValidLogin_ResultsInFailedLogin,IsFalse,LoginCommand_CanExecuteReturnsTrue_GivenValidUsernameAndPassword,Results,methods,babaandthepigman,blog,constructor,bool,boolean
Windows Live Tags: MVVM,Part,LoginCommand,password,text,spaces,Also,TestMethod,UsernameText_SetValue_ResultsInNewValue,MainViewModel,UsernameText,Assert,AreEqual,user,attention,expose,implementation,code,data,interface,ICommand,hook,framework,wire,RelayCommand,ExecuteLogin,CanExecuteLogin,magazine,clients,CanExecute,Execute,LoginCommand_ExecutedWithValidLogin_ResultsInSuccessfulLogin,behaviour,layer,separation,IUserRepository,discussion,Login,needs,reference,repository,IsLoggedIn,PasswordText,Where,Back,unit,Fake,FakeUserRepository,Func,LoginAction,region,logic,method,IsTrue,environment,Test,supports,BackgroundWorker,DoWork,BackgroundWorkerDoWork,RunWorkerAsync,Userdata,Username,sender,DoWorkEventArgs,argument,Result,RunWorkerCompletedEventArgs,VisualStateManager,GoToState,helper,credentials,background,worker,Asynchronous,EnqueueConditional,EnqueueCallback,EnqueueTestComplete,Enqueue,Sprinkle,LoginCommand_ExecutedWithInValidLogin_ResultsInFailedLogin,IsFalse,LoginCommand_CanExecuteReturnsTrue_GivenValidUsernameAndPassword,Results,methods,babaandthepigman,blog,constructor,bool,boolean
Comments