changeset 97:1adc1ae981ea

Tests added to SilverlightValidation.Tests
author stevenhollidge <stevenhollidge@hotmail.com>
date Sat, 05 May 2012 16:39:00 +0100
parents 188f8b366e87
children d0c2cac12376
files SilverlightValidation/SilverlightValidation.PL/ViewModels/UserViewModel.cs SilverlightValidation/SilverlightValidation.Tests/SilverlightValidation.Tests.csproj SilverlightValidation/SilverlightValidation.Tests/TestSupport/CommandCanExecuteAssertHelper.cs SilverlightValidation/SilverlightValidation.Tests/TestSupport/CommandCanExecuteChangedEventWatcher.cs SilverlightValidation/SilverlightValidation.Tests/TestSupport/Disposable.cs SilverlightValidation/SilverlightValidation.Tests/TestSupport/NotifyPropertyChangedAssertHelper.cs SilverlightValidation/SilverlightValidation.Tests/TestSupport/NotifyPropertyChangedEventWatcher.cs SilverlightValidation/SilverlightValidation.Tests/TestSupport/PropertySupport.cs SilverlightValidation/SilverlightValidation.Tests/ViewModels/NotifyPropertyChangedTester.cs SilverlightValidation/SilverlightValidation.Tests/ViewModels/UserViewModelTests.cs SilverlightValidation/SilverlightValidation.Tests/ViewModels/ViewModelBaseTests.cs SilverlightValidation/SilverlightValidation/App.xaml.cs
diffstat 12 files changed, 779 insertions(+), 18 deletions(-) [+]
line wrap: on
line diff
--- a/SilverlightValidation/SilverlightValidation.PL/ViewModels/UserViewModel.cs	Sat May 05 13:29:56 2012 +0100
+++ b/SilverlightValidation/SilverlightValidation.PL/ViewModels/UserViewModel.cs	Sat May 05 16:39:00 2012 +0100
@@ -27,6 +27,9 @@
 
         public UserViewModel(UserModel model, UserModelValidator validator)
         {
+            if (model == null) throw new ArgumentNullException("model");
+            if (validator == null) throw new ArgumentNullException("validator");
+
             _validator = validator;
             _data = model;
             _backup = model.Clone();
--- a/SilverlightValidation/SilverlightValidation.Tests/SilverlightValidation.Tests.csproj	Sat May 05 13:29:56 2012 +0100
+++ b/SilverlightValidation/SilverlightValidation.Tests/SilverlightValidation.Tests.csproj	Sat May 05 16:39:00 2012 +0100
@@ -31,11 +31,17 @@
     <WarningLevel>4</WarningLevel>
   </PropertyGroup>
   <ItemGroup>
+    <Reference Include="FluentValidation">
+      <HintPath>..\Libs\FluentValidation.dll</HintPath>
+    </Reference>
     <Reference Include="nunit.framework, Version=2.6.0.12051, Culture=neutral, PublicKeyToken=96d09a1eb7f44a77, processorArchitecture=MSIL">
-      <HintPath>..\packages\NUnit.2.6.0.12054\lib\nunit.framework.dll</HintPath>
+      <HintPath>..\Libs\nunit.framework.dll</HintPath>
     </Reference>
     <Reference Include="System" />
     <Reference Include="System.Core" />
+    <Reference Include="System.Windows">
+      <HintPath>..\..\..\..\Program Files (x86)\Reference Assemblies\Microsoft\Framework\Silverlight\v5.0\System.Windows.dll</HintPath>
+    </Reference>
     <Reference Include="System.Xml.Linq" />
     <Reference Include="System.Data.DataSetExtensions" />
     <Reference Include="Microsoft.CSharp" />
@@ -43,12 +49,23 @@
     <Reference Include="System.Xml" />
   </ItemGroup>
   <ItemGroup>
+    <Compile Include="TestSupport\PropertySupport.cs" />
+    <Compile Include="TestSupport\CommandCanExecuteAssertHelper.cs" />
+    <Compile Include="TestSupport\CommandCanExecuteChangedEventWatcher.cs" />
+    <Compile Include="TestSupport\Disposable.cs" />
+    <Compile Include="TestSupport\NotifyPropertyChangedAssertHelper.cs" />
+    <Compile Include="TestSupport\NotifyPropertyChangedEventWatcher.cs" />
+    <Compile Include="ViewModels\NotifyPropertyChangedTester.cs" />
     <Compile Include="ViewModels\UserListViewModelTests.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
     <Compile Include="ViewModels\UserViewModelTests.cs" />
+    <Compile Include="ViewModels\ViewModelBaseTests.cs" />
   </ItemGroup>
   <ItemGroup>
-    <None Include="packages.config" />
+    <ProjectReference Include="..\SilverlightValidation.PL\SilverlightValidation.PL.csproj">
+      <Project>{13B5F568-F402-4A2A-9A23-0FDF0B5564E3}</Project>
+      <Name>SilverlightValidation.PL</Name>
+    </ProjectReference>
   </ItemGroup>
   <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
   <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/SilverlightValidation/SilverlightValidation.Tests/TestSupport/CommandCanExecuteAssertHelper.cs	Sat May 05 16:39:00 2012 +0100
@@ -0,0 +1,162 @@
+using System;
+using System.Windows.Input;
+using NUnit.Framework;
+
+namespace SilverlightValidation.Tests.TestSupport
+{
+    public static class CommandCanExecuteAssertHelper
+    {
+        #region Assert methods (using helper)
+
+        #region Can/cannot execute
+
+        /// <summary>
+        /// Raises an assertion if the supplied command's CanExecute method returns false.
+        /// </summary>
+        /// <remarks>
+        /// CanExecute is passed null.
+        /// </remarks>
+        /// <param name="command">The command.</param>
+        public static void AssertCanExecute(this ICommand command)
+        {
+            if (command == null) throw new ArgumentNullException("command");
+
+            Assert.IsTrue(command.CanExecute(null));
+        }
+
+        /// <summary>
+        /// Raises an assertion if the supplied command's CanExecute method returns false.
+        /// </summary>
+        /// <typeparam name="T"></typeparam>
+        /// <param name="command">The command.</param>
+        /// <param name="value">A value to pass to CanExecute.</param>
+        public static void AssertCanExecute<T>(this ICommand command, T value)
+        {
+            if (command == null) throw new ArgumentNullException("command");
+
+            Assert.IsTrue(command.CanExecute(value));
+        }
+
+        /// <summary>
+        /// Raises an assertion if the supplied command's CanExecute method returns true.
+        /// </summary>
+        /// <remarks>
+        /// CanExecute is passed null.
+        /// </remarks>
+        /// <param name="command">The command.</param>
+        public static void AssertCannotExecute(this ICommand command)
+        {
+            if (command == null) throw new ArgumentNullException("command");
+
+            Assert.IsTrue(!command.CanExecute(null));
+        }
+
+        /// <summary>
+        /// Raises an assertion if the supplied command's CanExecute method returns true.
+        /// </summary>
+        /// <typeparam name="T"></typeparam>
+        /// <param name="command">The command.</param>
+        /// <param name="value">A value to pass to CanExecute.</param>
+        public static void AssertCannotExecute<T>(this ICommand command, T value)
+        {
+            if (command == null) throw new ArgumentNullException("command");
+
+            Assert.IsTrue(!command.CanExecute(value));
+        }
+
+        #endregion
+
+        #region Assert is raised/Assert is not raised (CommandCanExecute)
+
+        /// <summary>
+        /// Raises an assertion if the action is invoked and the supplied command
+        /// raises a CommandCanExecuteChanged event.
+        /// </summary>
+        /// <param name = "command">The command.</param>
+        /// <param name = "action">The action to invoke.</param>
+        public static void AssertIsNotRaised(this ICommand command, Action action)
+        {
+            Assert.IsTrue(IsNotRaised(command, action));
+        }
+
+        /// <summary>
+        /// Raises an assertion if the action is invoked and the supplied command
+        /// does not raise at least one CommandCanExecuteChanged event.
+        /// </summary>
+        /// <param name = "command">The command.</param>
+        /// <param name = "action">The action to invoke.</param>
+        public static void AssertIsRaised(this ICommand command, Action action)
+        {
+            Assert.IsTrue(IsRaised(command, action));
+        }
+
+        /// <summary>
+        /// Raises an assertion if the action is invoked and the supplied command
+        /// does raise the number of CommandCanExecuteChanged events as defined by the 
+        /// supplied predicate.
+        /// </summary>
+        /// <param name = "command">The command.</param>
+        /// <param name = "predicate">The predicate.</param>
+        /// <param name = "action">The action to invoke.</param>
+        public static void AssertIsRaised(this ICommand command, Predicate<int> predicate, Action action)
+        {
+            Assert.IsTrue(IsRaised(command, predicate, action));
+        }
+
+        #endregion
+
+        #endregion
+
+        #region Is raised/Is not raised methods (CommandCanExecute)
+
+        /// <summary>
+        ///   Determines if the specified CommandCanExecuteChanged event is
+        ///   not raised when the supplied action is invoked.
+        /// </summary>
+        /// <param name = "command">The command.</param>
+        /// <param name = "action">The action to invoke.</param>
+        public static bool IsNotRaised(this ICommand command, Action action)
+        {
+            return !IsRaised(command, action);
+        }
+
+        /// <summary>
+        ///   Determines if the specified CommandCanExecuteChanged event is
+        ///   raised one or more times when the supplied action is invoked.
+        /// </summary>
+        /// <param name = "command">The command.</param>
+        /// <param name = "action">The action to invoke.</param>
+        /// <returns>
+        ///   <c>true</c> if the specified command is raised; otherwise, <c>false</c>.
+        /// </returns>
+        public static bool IsRaised(this ICommand command, Action action)
+        {
+            return IsRaised(command, count => count > 0, action);
+        }
+
+        /// <summary>
+        ///   Determines if the specified CommandCanExecuteChanged event is 
+        ///   raised the number of times as defined by the predicate when the supplied action is invoked.
+        /// </summary>
+        /// <param name = "command">The command.</param>
+        /// <param name = "predicate">A predicate.</param>
+        /// <param name = "action">The action to invoke.</param>
+        /// <returns>
+        ///   <c>true</c> if the specified command's CanExecuteChanged event is raised; otherwise, <c>false</c>.
+        /// </returns>
+        public static bool IsRaised(this ICommand command, Predicate<int> predicate, Action action)
+        {
+            if (predicate == null) throw new ArgumentNullException("predicate");
+            if (action == null) throw new ArgumentNullException("action");
+
+            using (var watcher = new CommandCanExecuteChangedEventWatcher(command))
+            {
+                action();
+
+                return predicate(watcher.RaisedCount);
+            }
+        }
+
+        #endregion
+    }
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/SilverlightValidation/SilverlightValidation.Tests/TestSupport/CommandCanExecuteChangedEventWatcher.cs	Sat May 05 16:39:00 2012 +0100
@@ -0,0 +1,51 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Windows.Input;
+
+namespace SilverlightValidation.Tests.TestSupport
+{
+    [SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses")]
+    internal sealed class CommandCanExecuteChangedEventWatcher : Disposable
+    {
+        private ICommand _command;
+
+        /// <summary>
+        ///   Initializes a new instance of the <see cref = "CommandCanExecuteChangedEventWatcher" /> class.
+        /// </summary>
+        /// <param name = "command">The command.</param>
+        public CommandCanExecuteChangedEventWatcher(ICommand command)
+        {
+            if (command == null)
+            {
+                throw new ArgumentNullException("command");
+            }
+
+            _command = command;
+            _command.CanExecuteChanged += CommandCanExecuteChanged;
+        }
+
+        private void CommandCanExecuteChanged(object sender, EventArgs e)
+        {
+            RaisedCount++;
+        }
+
+        /// <summary>
+        ///   Gets the number of times CanExecuteChanged was raised for the monitored command.
+        /// </summary>
+        public int RaisedCount { get; private set; }
+
+        /// <summary>
+        ///   Releases unmanaged and - optionally - managed resources
+        /// </summary>
+        /// <param name = "disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+        protected override void Dispose(bool disposing)
+        {
+            if (_command != null)
+            {
+                var command = _command;
+                _command = null;
+                command.CanExecuteChanged -= CommandCanExecuteChanged;
+            }
+        }
+    }
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/SilverlightValidation/SilverlightValidation.Tests/TestSupport/Disposable.cs	Sat May 05 16:39:00 2012 +0100
@@ -0,0 +1,32 @@
+using System;
+
+namespace SilverlightValidation.Tests.TestSupport
+{
+    public abstract class Disposable : IDisposable
+    {
+        /// <summary>
+        /// A base class for disposable classes.
+        /// </summary>
+        public void Dispose()
+        {
+            Dispose(true);
+
+            GC.SuppressFinalize(this);
+        }
+
+        /// <summary>
+        /// Releases unmanaged and - optionally - managed resources
+        /// </summary>
+        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+        protected abstract void Dispose(bool disposing);
+
+        /// <summary>
+        /// Releases unmanaged resources and performs other cleanup operations before the
+        /// <see cref="Disposable"/> is reclaimed by garbage collection.
+        /// </summary>
+        ~Disposable()
+        {
+            Dispose(false);
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/SilverlightValidation/SilverlightValidation.Tests/TestSupport/NotifyPropertyChangedAssertHelper.cs	Sat May 05 16:39:00 2012 +0100
@@ -0,0 +1,260 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Linq.Expressions;
+using NUnit.Framework;
+
+namespace SilverlightValidation.Tests.TestSupport
+{
+    /// <summary>
+    ///   A utility class containing methods that verify whether a property changed event 
+    ///   is raised/not raised for a specified model property, on invocation of a specified Action.
+    /// </summary>
+    /// <remarks>
+    ///   An example of usage in conjunction with NUnit:
+    ///   <code>
+    ///     var model = new CustomerViewModel();
+    ///     Assert.That(NotifyPropertyChangedAssertHelper.IsRaised(model, () => model.Name = "foo", () => model.Name, count => count == 1)
+    ///   </code>
+    ///   Here the action is model.Name = "foo",
+    ///   the monitored property is the "Name" property, specified by the lambda expression "() => model.Name",
+    ///   and the test for success is "count == 1".
+    /// </remarks>
+    public static class NotifyPropertyChangedAssertHelper
+    {
+        #region Assert methods (using AssertionProxy)
+
+        #region Is raised
+
+        [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures")]
+        public static void AssertIsRaised<T, T1>(this T model, Expression<Func<T, T1>> propertyExpression, Action action)
+            where T : INotifyPropertyChanged
+        {
+            Assert.IsTrue(IsRaised(model, propertyExpression, action));
+        }
+
+        public static void AssertIsRaised(this INotifyPropertyChanged model, string propertyName, Action action)
+        {
+            Assert.IsTrue(IsRaised(model, propertyName, action));
+        }
+
+        [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures")]
+        public static void AssertIsRaised<T, T1>(
+            this T model, Expression<Func<T, T1>> propertyExpression, Predicate<int> predicate, Action action)
+            where T : INotifyPropertyChanged
+        {
+            Assert.IsTrue(IsRaised(model, propertyExpression, predicate, action));
+        }
+
+        public static void AssertIsRaised(this INotifyPropertyChanged model, string propertyName,
+                                          Predicate<int> predicate, Action action)
+        {
+            Assert.IsTrue(IsRaised(model, propertyName, predicate, action));
+        }
+
+        #endregion
+
+        #region Are raised
+
+        public static void AssertAreRaised(this INotifyPropertyChanged model, IEnumerable<string> propertyNames,
+                                           Action action)
+        {
+            Assert.IsTrue(AreRaised(model, propertyNames, action));
+        }
+
+        [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures")]
+        public static void AssertAreRaised(this INotifyPropertyChanged model,
+                                           IDictionary<string, Predicate<int>> criteria, Action action)
+        {
+            Assert.IsTrue(AreRaised(model, criteria, action));
+        }
+
+        #endregion
+
+        #region Is not raised
+
+        [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures")]
+        public static void AssertIsNotRaised<T, T1>(this T model, Expression<Func<T, T1>> propertyExpression,
+                                                    Action action)
+            where T : INotifyPropertyChanged
+        {
+            Assert.IsTrue(IsNotRaised(model, propertyExpression, action));
+        }
+
+        public static void AssertIsNotRaised(this INotifyPropertyChanged model, Action action, string propertyName)
+        {
+            Assert.IsTrue(IsNotRaised(model, action, propertyName));
+        }
+
+        #endregion
+
+        #region Are not raised
+
+        public static void AssertAreNotRaised(this INotifyPropertyChanged model, IEnumerable<string> propertyNames,
+                                              Action action)
+        {
+            Assert.IsTrue(AreNotRaised(model, propertyNames, action));
+        }
+
+        #endregion
+
+        #endregion
+
+        #region Is and Are methods
+
+        #region Is raised
+
+        /// <summary>
+        ///   Determines whether the specified property changed event is raised.
+        /// </summary>
+        /// <returns>
+        ///   <c>true</c> if the specified property changed event is raised at least once; otherwise, <c>false</c>.
+        /// </returns>
+        [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters"),
+         SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures")]
+        public static bool IsRaised<T, T1>(this T model, Expression<Func<T, T1>> propertyExpression, Action action)
+            where T : INotifyPropertyChanged
+        {
+            return IsRaised(model, PropertySupport.ExtractPropertyName(propertyExpression), action);
+        }
+
+        /// <summary>
+        ///   Determines whether the specified property changed event is raised.
+        /// </summary>
+        /// <returns>
+        ///   <c>true</c> if the specified property changed event is raised at least once; otherwise, <c>false</c>.
+        /// </returns>
+        public static bool IsRaised(this INotifyPropertyChanged model, string propertyName, Action action)
+        {
+            return AreRaised(model, new[] {propertyName}, action);
+        }
+
+        /// <summary>
+        ///   Determines whether the specified property changed event is raised.
+        /// </summary>
+        /// <returns>
+        ///   <c>true</c> if the specified property changed event count satisfies the supplied predicate; otherwise, <c>false</c>.
+        /// </returns>
+        [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters"),
+         SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures")]
+        public static bool IsRaised<T, T1>(this T model, Expression<Func<T, T1>> propertyExpression,
+                                           Predicate<int> predicate, Action action)
+            where T : INotifyPropertyChanged
+        {
+            return IsRaised(model, PropertySupport.ExtractPropertyName(propertyExpression), predicate, action);
+        }
+
+        /// <summary>
+        ///   Determines whether the specified property changed event is raised.
+        /// </summary>
+        /// <returns>
+        ///   <c>true</c> if the specified property changed event count satisfies the supplied predicate; otherwise, <c>false</c>.
+        /// </returns>
+        public static bool IsRaised(this INotifyPropertyChanged model, string propertyName, Predicate<int> predicate,
+                                    Action action)
+        {
+            if (action == null)
+            {
+                throw new ArgumentNullException("action");
+            }
+
+            if (predicate == null)
+            {
+                throw new ArgumentNullException("predicate");
+            }
+
+            return AreRaised(model, new Dictionary<string, Predicate<int>> {{propertyName, predicate}}, action);
+        }
+
+        #endregion
+
+        #region Are raised
+
+        /// <summary>
+        ///   Determines whether each of the specified properties has at least one NotifyPropertyChanged event raised.
+        /// </summary>
+        public static bool AreRaised(this INotifyPropertyChanged model, IEnumerable<string> propertyNames, Action action)
+        {
+            return AreRaised(model, propertyNames.ToDictionary<string, string, Predicate<int>>(p => p, p => c => c > 0),
+                             action);
+        }
+
+        /// <summary>
+        ///   Determines whether the specified property changed events are raised.
+        /// </summary>
+        /// <returns>
+        ///   <c>true</c> if the specified property changed event count satisfies the supplied predicate for each property; otherwise, <c>false</c>.
+        /// </returns>
+        [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures")]
+        public static bool AreRaised(this INotifyPropertyChanged model, IDictionary<string, Predicate<int>> criteria,
+                                     Action action)
+        {
+            if (action == null)
+            {
+                throw new ArgumentNullException("action");
+            }
+
+            if (criteria == null)
+            {
+                throw new ArgumentNullException("criteria");
+            }
+
+            using (var watcher = new NotifyPropertyChangedEventWatcher(model))
+            {
+                action();
+
+                return criteria.All(pair => pair.Value(watcher.GetRaisedCount(pair.Key)));
+            }
+        }
+
+        #endregion
+
+        #region Is not raised
+
+        /// <summary>
+        ///   Determines whether the specified property changed event is not raised.
+        /// </summary>
+        /// <returns>
+        ///   <c>true</c> if the specified property changed event is raised zero times; otherwise, <c>false</c>.
+        /// </returns>
+        [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters"),
+         SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures")]
+        public static bool IsNotRaised<T, T1>(this T model, Expression<Func<T, T1>> propertyExpression, Action action)
+            where T : INotifyPropertyChanged
+        {
+            return IsNotRaised(model, action, PropertySupport.ExtractPropertyName(propertyExpression));
+        }
+
+        /// <summary>
+        ///   Determines whether the specified property changed event is not raised.
+        /// </summary>
+        /// <returns>
+        ///   <c>true</c> if the specified property changed event is raised zero times; otherwise, <c>false</c>.
+        /// </returns>
+        public static bool IsNotRaised(this INotifyPropertyChanged model, Action action, string propertyName)
+        {
+            return AreNotRaised(model, new[] {propertyName}, action);
+        }
+
+        #endregion
+
+        #region Are not raised
+
+        /// <summary>
+        ///   Determines whether all of the specified properties have changed events raised.
+        /// </summary>
+        public static bool AreNotRaised(this INotifyPropertyChanged model, IEnumerable<string> propertyNames,
+                                        Action action)
+        {
+            return AreRaised(model,
+                             propertyNames.ToDictionary<string, string, Predicate<int>>(p => p, p => c => c == 0),
+                             action);
+        }
+
+        #endregion
+
+        #endregion
+    }
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/SilverlightValidation/SilverlightValidation.Tests/TestSupport/NotifyPropertyChangedEventWatcher.cs	Sat May 05 16:39:00 2012 +0100
@@ -0,0 +1,103 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Linq.Expressions;
+
+namespace SilverlightValidation.Tests.TestSupport
+{
+    /// <summary>
+    ///   A helper class used by NotifyPropertyChangedAssertHelper to monitor PropertyChangedEvent notifications.
+    /// </summary>
+    public class NotifyPropertyChangedEventWatcher : Disposable
+    {
+        private readonly Dictionary<string, int> _raisedCounts = new Dictionary<string, int>();
+
+        // A reference to the instance of the class implementing INotifyPropertyChanged
+        private INotifyPropertyChanged _model;
+
+        /// <summary>
+        ///   Initializes a new instance of the <see cref = "NotifyPropertyChangedEventWatcher" /> class.
+        /// </summary>
+        /// <param name = "model">The model.</param>
+        public NotifyPropertyChangedEventWatcher(INotifyPropertyChanged model)
+        {
+            if (model == null) throw new ArgumentNullException("model");
+
+            _model = model;
+            _model.PropertyChanged += OnPropertyChanged;
+        }
+
+        [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters"),
+         SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures")]
+        public int GetRaisedCount<T>(Expression<Func<T>> propertyExpression)
+        {
+            return GetRaisedCount(PropertySupport.ExtractPropertyName(propertyExpression));
+        }
+
+        public int GetRaisedCount(string propertyName)
+        {
+            propertyName = string.IsNullOrEmpty(propertyName) ? string.Empty : propertyName;
+
+            PropertySupport.VerifyPropertyName(_model, propertyName);
+
+            return _raisedCounts.ContainsKey(propertyName) ? _raisedCounts[propertyName] : 0;
+        }
+
+        private void RecordPropertyChanged(string propertyName)
+        {
+            if (_raisedCounts.ContainsKey(propertyName))
+            {
+                _raisedCounts[propertyName]++;
+            }
+            else
+            {
+                _raisedCounts[propertyName] = 1;
+            }
+        }
+
+        /// <summary>
+        ///   The OnPropertyChanged event handler.
+        /// </summary>
+        /// <remarks>
+        ///   Records how many times the monitored property name has changed.
+        /// </remarks>
+        private void OnPropertyChanged(object sender, PropertyChangedEventArgs eventArgs)
+        {
+            // If PropertyName is null or string.Empty then it's the 'all properties' changed event.
+            RecordPropertyChanged(string.IsNullOrEmpty(eventArgs.PropertyName) ? string.Empty : eventArgs.PropertyName);
+        }
+
+        /// <summary>
+        ///   Releases unmanaged and - optionally - managed resources
+        /// </summary>
+        /// <param name = "disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+        protected override void Dispose(bool disposing)
+        {
+            if (null != _model)
+            {
+                var model = _model;
+                _model = null;
+                model.PropertyChanged -= OnPropertyChanged;
+            }
+        }
+
+        /// <summary>
+        ///   Traces the event counts.
+        /// </summary>
+        [Conditional("DEBUG")]
+        public void TraceEventCounts()
+        {
+            foreach (var pair in _raisedCounts)
+            {
+                string name = string.IsNullOrEmpty(pair.Key) ? "all properties" : pair.Key;
+                int value = pair.Value;
+
+                Debug.WriteLine(string.Format(
+                    CultureInfo.InvariantCulture, @"Property '{0}' raised {1} times.", name, value));
+            }
+        }
+    }
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/SilverlightValidation/SilverlightValidation.Tests/TestSupport/PropertySupport.cs	Sat May 05 16:39:00 2012 +0100
@@ -0,0 +1,61 @@
+using System;
+using System.Diagnostics;
+using System.Linq.Expressions;
+using System.Reflection;
+
+namespace SilverlightValidation.Tests.TestSupport
+{
+    /// <summary>
+    /// Utility class with property specific helper methods.
+    /// </summary>
+    public static class PropertySupport
+    {
+        [Conditional("DEBUG")]
+        [DebuggerStepThrough]
+        public static void VerifyPropertyName<T>(T model, string propertyName)
+        {
+            // A null or empty string indicates all properties on the object have changed
+            if (!string.IsNullOrEmpty(propertyName))
+            {
+                var modelType = model.GetType();
+                if (modelType.GetProperty(propertyName) == null)
+                {
+                    throw new ArgumentException(@"Property not found on target type", propertyName);
+                }
+            }
+        }
+
+        /// <summary>
+        /// Extracts the name of a property from a suitable LambdaExpression.
+        /// </summary>
+        /// <param name="propertyExpression">The property expression.</param>
+        /// <returns></returns>
+        public static string ExtractPropertyName(LambdaExpression propertyExpression)
+        {
+            if (propertyExpression == null)
+            {
+                throw new ArgumentNullException("propertyExpression");
+            }
+
+            var memberExpression = propertyExpression.Body as MemberExpression;
+            if (memberExpression == null)
+            {
+                throw new ArgumentException(@"Not a member expression", "propertyExpression");
+            }
+
+            var property = memberExpression.Member as PropertyInfo;
+            if (property == null)
+            {
+                throw new ArgumentException(@"Not a property", "propertyExpression");
+            }
+
+            var getMethod = property.GetGetMethod(true);
+            if (getMethod.IsStatic)
+            {
+                throw new ArgumentException(@"Can't be static", "propertyExpression");
+            }
+
+            return memberExpression.Member.Name;
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/SilverlightValidation/SilverlightValidation.Tests/ViewModels/NotifyPropertyChangedTester.cs	Sat May 05 16:39:00 2012 +0100
@@ -0,0 +1,43 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using NUnit.Framework;
+
+namespace SilverlightValidation.Tests.ViewModels
+{
+    public class NotifyPropertyChangedTester
+    {
+        public NotifyPropertyChangedTester(INotifyPropertyChanged viewModel)
+        {
+            if (viewModel == null) throw new ArgumentNullException("viewModel");
+
+            this.Changes = new List<string>();
+
+            viewModel.PropertyChanged += viewModel_PropertyChanged;
+        }
+
+        private void viewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
+        {
+            Changes.Add(e.PropertyName);
+        }
+
+        public List<string> Changes { get; private set; }
+
+        public void AssertChange(int changeIndex, string expectedPropertyName)
+        {
+            Assert.IsNotNull(Changes);
+
+            Assert.IsTrue(changeIndex < Changes.Count,
+                          "Changes collection contains '{0}' items and does not have an element at index '{1}'.",
+                          Changes.Count,
+                          changeIndex);
+
+            Assert.AreEqual(expectedPropertyName,
+                            Changes[changeIndex],
+                            "Change at index '{0}' is '{1}' and is not equal to '{2}'.",
+                            changeIndex,
+                            Changes[changeIndex],
+                            expectedPropertyName);
+        }
+    }
+}
--- a/SilverlightValidation/SilverlightValidation.Tests/ViewModels/UserViewModelTests.cs	Sat May 05 13:29:56 2012 +0100
+++ b/SilverlightValidation/SilverlightValidation.Tests/ViewModels/UserViewModelTests.cs	Sat May 05 16:39:00 2012 +0100
@@ -1,14 +1,42 @@
-using NUnit.Framework;
+using System;
+using NUnit.Framework;
+using SilverlightValidation.Models;
+using SilverlightValidation.Validators;
+using SilverlightValidation.ViewModels;
 
 namespace SilverlightValidation.Tests.ViewModels
 {
     [TestFixture]
     class UserViewModelTests
     {
+        #region constructor tests
+
         [Test]
-        public void Given_When_Then()
+        public void Constructor_WhenTwoNulls_ThenArgumentNullExceptionForModel()
+        {
+            Assert.Throws<ArgumentNullException>(() => new UserViewModel(null, null), "model");
+        }
+
+        [Test]
+        public void Constructor_WhenFirstParameterIsNull_ThenArgumentNullExceptionForModel()
         {
-            Assert.True(true);
+            Assert.Throws<ArgumentNullException>(() => new UserViewModel(null, new UserModelValidator()), "model");
+        }
+
+        [Test]
+        public void Constructor_WhenSecondParameterIsNull_ThenArgumentNullExceptionForValidator()
+        {
+            Assert.Throws<ArgumentNullException>(() => new UserViewModel(new UserModel(), null), "validator");
         }
+
+        [Test]
+        public void DateOfBirth_WhenUpdated_ThenFiresPropertyChangeEvent()
+        {
+            var vm = new UserViewModel(new UserModel(), new UserModelValidator());
+            var tester = new NotifyPropertyChangedTester(vm);
+            //tester.AssertChange();
+        }
+
+        #endregion
     }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/SilverlightValidation/SilverlightValidation.Tests/ViewModels/ViewModelBaseTests.cs	Sat May 05 16:39:00 2012 +0100
@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using NUnit.Framework;
+
+namespace SilverlightValidation.Tests.ViewModels
+{
+    [TestFixture]
+    class ViewModelBaseTests
+    {
+        
+    }
+}
--- a/SilverlightValidation/SilverlightValidation/App.xaml.cs	Sat May 05 13:29:56 2012 +0100
+++ b/SilverlightValidation/SilverlightValidation/App.xaml.cs	Sat May 05 16:39:00 2012 +0100
@@ -1,9 +1,5 @@
 using System;
 using System.Windows;
-using System.Windows.Controls;
-using System.Windows.Navigation;
-//using SilverlightGlimpse.Services;
-using System.Diagnostics;
 
 namespace SilverlightValidation
 {
@@ -20,15 +16,7 @@
 
         private void Application_Startup(object sender, StartupEventArgs e)
         {
-            try
-            {
-                this.RootVisual = new Views.UserListView();
-                //GlimpseService.CreateInstance.Load(this, "Silverlight Glimpse");
-            }
-            catch (Exception ex)
-            {
-                //GlimpseService.CreateInstance.DisplayLoadFailure(this, ex, "Glimpse Demo");
-            }
+            this.RootVisual = new Views.UserListView();
         }
 
         private void Application_Exit(object sender, EventArgs e)