52 Weeks of Xamarin: Week 8 – Testing the View Model

Until now, our tests have focused on the model, Projects.  The point of MVVM, however, is to enable sufficient separation of concerns to allow testing of the program’s logic in the ViewModel.

Today, we turn to ViewModel tests, using these tests both to drive the development of the ViewModel, and also to ensure the integrity and correctness of the logic as we refactor the code.

         [Fact]
         public void ConstructorStartsTimerWithOneSecondInterval() {
             var viewModel = getViewModel();
             Assert.Equal(1, timer.Interval.TotalSeconds);
         }
 

Our first test depends on a helper method, getViewModel():

 private ProjectTrackerViewModel getViewModel(
     params string[] projectNames) {
     var viewModel = new ProjectTrackerViewModel(timer);
 
     foreach (var projectName in projectNames) {
         viewModel.NewProjectName = projectName;
         viewModel.AddCommand.Execute(null);
     }
     return viewModel;
 }
 

The timer that is passed into the VM constructor is a member of the class of type MockTimer,

 private readonly MockTimer timer;

The MockTimer class is defined as an implementation of ITimer,

public interface ITimer { 
     DateTime UtcNow { get; }
 
     void Start(TimeSpan interval);
 
     event EventHandler<DateTime> Elapsed;
 }

MockTimer itself is defined in the tests as follows,

 public class MockTimer : ITimer {
 
 public event EventHandler<DateTime> Elapsed;
 
 public TimeSpan Interval { get; private set; }
 
 public void Start(TimeSpan interval) {
     Interval = interval;
 }
 
 private DateTime utcNow;
 
 public DateTime UtcNow {
     get { return utcNow; }
     set {
         utcNow = value;
         Elapsed?.Invoke(this, UtcNow);
     }
 }
 
 public void AdvanceSeconds(int seconds) {
     UtcNow = UtcNow.AddSeconds(seconds);
 }

To be clear, ITimer is defined in the working project, while MockTimer is declared in the tests.

The ViewModel that falls out of this test (and the commands we already defined) looks like this:

 public class ProjectTrackerViewModel : BindableObjectBase {
 
 private readonly ITimer timer;
 private Project currentProject;
 
 public ProjectTrackerViewModel(ITimer timer) {
     this.timer = timer;
 
 
     AddCommand = new Command(addProject);
     ResetCommand = new Command(reset);
     ToggleProjectCommand = new Command<Project>(toggleProject);
 
     timer.Start(TimeSpan.FromSeconds(1));
     timer.Elapsed += (sender, tickUtc) => currentProject?.Tick(tickUtc);
 
 
 }
 
 public ICommand AddCommand { get; }
 
 public ICommand ResetCommand { get; }
 
 public ICommand ToggleProjectCommand { get; }
 

More Tests

Returning to our tests, our next VM test ensures that if we try to create a project with an empty project name, nothing is created,

 [Fact]
 public void AddCommandEmptyProjectNameDoesNothing() {
     var viewModel = getViewModel();
     viewModel.NewProjectName = string.Empty;
     viewModel.AddCommand.Execute(null);
     Assert.Empty(viewModel.Projects);
 }
 

Now we turn to the meat of the logic.  First, we create a test named AddCommandNoActiveProjectsAddsNewInactiveProject.  That is, if we call the Add command and there are currently no active projects, we get a new inactive project,

 [Fact]
 public void AddCommandNoActiveProjectsAddsNewInactiveProject() {
     var viewModel = getViewModel();
     var projectName = "ProjectTracker";
     viewModel.NewProjectName = projectName;
     viewModel.AddCommand.Execute(null);
 
     Assert.Equal(1, viewModel.Projects.Count);
     Assert.Equal(projectName, viewModel.Projects[0].Name);
     Assert.False(viewModel.Projects[0].IsActive);
     Assert.Equal(string.Empty, viewModel.NewProjectName);
 }
 

To make this test green, we need to add to our view model.  For the sake of this posting, rather than building up the VM step by step, here is the completed ViewModel,

 using System;
 using Xamarin.Forms;
 using System.Windows.Input;
 using System.Collections.ObjectModel;
 
 namespace ProjectTracker {
 public class ProjectTrackerViewModel : BindableObjectBase {
 
 private readonly ITimer timer;
 private Project currentProject;
 
 public ProjectTrackerViewModel(ITimer timer) {
     this.timer = timer;
 
 
     AddCommand = new Command(addProject);
     ResetCommand = new Command(reset);
     ToggleProjectCommand = new Command<Project>(toggleProject);
 
     timer.Start(TimeSpan.FromSeconds(1));
     timer.Elapsed += (sender, tickUtc) => currentProject?.Tick(tickUtc);
 
 
 }
 
 public ICommand AddCommand { get; }
 
 public ICommand ResetCommand { get; }
 
 public ICommand ToggleProjectCommand { get; }
 
         private ObservableCollection<Project> projects =
             new ObservableCollection<Project>();
 
         public ObservableCollection<Project> Projects {
             get { return projects; }
             set {
                 projects = value;
                 RaisePropertyChanged(nameof(Projects));
             }
         }
 
         private string newProjectName;
 
         public string NewProjectName {
             get { return newProjectName; }
             set {
                 newProjectName = value;
                 RaisePropertyChanged(nameof(NewProjectName));
             }
         }
 
         private void reset() {
             foreach (var project in Projects)
                 project.Reset();
         }
 
         private void toggleProject(Project project) {
             if (currentProject == project) {
                 currentProject.StopTracking(timer.UtcNow);
                 currentProject = null;
             } else if (currentProject == null) {
                 currentProject = project;
                 currentProject.StartTracking(timer.UtcNow);
             } else {
                 currentProject.StopTracking(timer.UtcNow);
                 currentProject = project;
                 currentProject.StartTracking(timer.UtcNow);
             }
         }
 
         private void addProject() {
             if (string.IsNullOrWhiteSpace(NewProjectName))
                 return;
 
             Projects.Add(new Project(NewProjectName));
             NewProjectName = "";
         }
 
     }
 }
 
 

Let’s review some of the tests that lead to this VM.  the next test ensures that if you click the Add button and there is an existing active project, it leaves that project active and adds a new inactive project (thus, adding a project does not make that project “live”).

 [Fact]
 public void AddCommandExistingActiveProjectLeavesExistingProjectActiveAndAddsNewInactiveProject() {
     var viewModel = getViewModel("first project");
     var firstProject = viewModel.Projects.Single();
 
     viewModel.ToggleProjectCommand.Execute(firstProject);
     Assert.True(firstProject.IsActive);
 
     viewModel.NewProjectName = "second project";
     viewModel.AddCommand.Execute(null);
 
     Assert.Equal(2, viewModel.Projects.Count);
     Assert.True(viewModel.Projects[0].IsActive);
     Assert.False(viewModel.Projects[1].IsActive);
 }
 

Now let’s turn to the timer.  We need a test that when time elapses only one project at a time gets updated,

        [Fact]
        public void TimeElapsed_OneProjectActive_ActiveProjectGetsTime() {
            var startingTime = new DateTime(2015, 9, 7, 0, 0, 0);
            var secondsToAdvance = 10;
            timer.UtcNow = startingTime;

            var viewModel = getViewModel(first project, second project);
            var firstProject = viewModel.Projects.First();
            var secondProject = viewModel.Projects.Last();
            viewModel.ToggleProjectCommand.Execute(firstProject);

            timer.AdvanceSeconds(secondsToAdvance);

            Assert.Equal(secondsToAdvance, firstProject.TotalDurationSeconds);
            Assert.Equal(0, secondProject.TotalDurationSeconds);
        }

 

And so it goes, one test after another to build and test the View Model.  The running application looks like this:

Project Tracker

 

 

About Jesse Liberty

Jesse Liberty has three decades of experience writing and delivering software projects and is the author of 2 dozen books and a couple dozen online courses. His latest book, Building APIs with .NET will be released early in 2025. Liberty is a Senior SW Engineer for CNH and he was a Senior Technical Evangelist for Microsoft, a Distinguished Software Engineer for AT&T, a VP for Information Services for Citibank and a Software Architect for PBS. He is a Microsoft MVP.
This entry was posted in Testing, Xamarin. Bookmark the permalink.