Testing multi-threaded WPF code
Stop press: Dont use this method. Read the article, but follow comment at bottom of post (ie Use Rx instead)In the last post in this series we took a rather long way to get to the multi-threaded result we were looking for. This was because we decided to separate out our code in cohesive parts that allowed for more maintainable code. The popular method of creating maintainable code currently is through Unit Testing. An even better approach would be to apply Test Driven Development. TDD is a style of design where tests outlining our goals are written first and then code to satisfy those tests is created. Having identified the technical challenges of keeping a WPF application responsive, namely; the concepts related to the Dispatcher and parallel programming, we can now step back again to see the bigger picture as we did in part 4, to move forward.
In this post we will drive out our presentation model through tests and discover the issues related to testing WPF code and in particular testing the multi-threaded code. Just to set expectations, there is an assumption here that the reader is familiar with WPF Data Templates and unit testing with a mocking framework.
From my experience with unit testing it is always good to start off with a set of requirements that we could turn into tests. Continuing with our example of a thumbnail viewer, lets create a list of our requirements:
- User can choose a folder
- Can see what folder was selected
- Will list all images in that directory and sub directories
- Will indicate when it is processing
- Cannot set folder while processing
- The user interface will remain responsive during processing
For brevity, I will try to cheat as much as I can so more time can be spent on the technical issues of testing multi-threaded WPF code and less time spouting TDD goodness. I will first cheat by making the assumption that this is WPF code so will need implement constructs such as ICommand and INotifyPropertyChanged.
From these requirements I eventually came to the following test cases:
[TestClass] class PhotoAlbumModelFixture { [TestMethod] public void SetSourceCommand_sets_SourcePath(){} [TestMethod] public void SetSourceCommand_calls_FileService_with_value_from_FolderSelector(){} [TestMethod] public void SetSourceCommand_calls_FileService_with_an_ImageOnly_filter() {} [TestMethod] public void SetSourceCommand_populates_images_from_FileService(){} [TestMethod] public void SetSourceCommand_recursively_calls_FileService(){} [TestMethod] public void SetSourceCommand_does_not_call_FileService_with_empty_value(){} [TestMethod] public void SetSourceCommand_sets_IsLoading_to_True(){} [TestMethod] public void SetSourceCommand_is_disabled_when_IsLoading(){} [TestMethod] public void FileService_is_called_Async(){} }
When writing some of the tests I found that standard unit test code was fine. When I started to implement my code that had to make multithreaded calls, however, testing became problematic.
For example, my first attempt to write the "SetSourceCommand_calls_FileService_with_value_from_FolderSelector" test looked like this:
[TestMethod] public void SetSourceCommand_calls_FileService_with_value_from_FolderSelector() { Rhino.Mocks.MockRepository mockery = new Rhino.Mocks.MockRepository(); ArtemisWest.Demo.ResponsiveUI._4_MultiThreaded.IFileService fileService = mockery.DynamicMock<ArtemisWest.Demo.ResponsiveUI._4_MultiThreaded.IFileService>(); ArtemisWest.Demo.ResponsiveUI._5_MultiThreadedTest.IFolderSelector folderSelector = mockery.DynamicMock<ArtemisWest.Demo.ResponsiveUI._5_MultiThreadedTest.IFolderSelector>(); PhotoAlbumModel model = new PhotoAlbumModel(Dispatcher.CurrentDispatcher, fileService, folderSelector); using (mockery.Record()) { Expect.Call(folderSelector.SelectFolder()).Return(FolderPath); Expect.Call(fileService.ListFiles(FolderPath, null)).Return(EmptyList).IgnoreArguments().Constraints( Rhino.Mocks.Constraints.Is.Equal(FolderPath), Rhino.Mocks.Constraints.Is.Anything()); Expect.Call(fileService.ListDirectories(FolderPath)).Return(EmptyList); } using (mockery.Playback()) { model.SetSourcePathCommand.Execute(null); } }
And following TDD rules of just write enough code to satisfy the test, my implementation of the Execute command handler looks like this:
void ExecuteSetSourcePath(object sender, ExecutedEventArgs e) { SourcePath = this.folderSelector.SelectFolder(); IsLoading = true; LoadPath(SourcePath); IsLoading = false; } void LoadPath(string path) { IEnumerable<string> files = fileService.ListFiles(path, IsImage); foreach (string item in files) { Images.Add(item); } IEnumerable<string> directories = fileService.ListDirectories(path); foreach (string item in directories) { LoadPath(item); } }
Astute readers will notice the complete lack of "Responsive UI Code". Well I have yet to reach my test that specifies that the call to FileService should be asynchronous. So after I have finished writing all of my other tests and the code to support them, I write some test code to enforce the responsive requirement:
[TestMethod] public void FileService_is_called_Async() { SlowFileServiceFake fileService = new SlowFileServiceFake(); Rhino.Mocks.MockRepository mockery = new Rhino.Mocks.MockRepository(); ArtemisWest.Demo.ResponsiveUI._5_MultiThreadedTest.IFolderSelector folderSelector = mockery.DynamicMock<ArtemisWest.Demo.ResponsiveUI._5_MultiThreadedTest.IFolderSelector>(); PhotoAlbumModel model = new PhotoAlbumModel(Dispatcher.CurrentDispatcher, fileService, folderSelector); using (mockery.Record()) { Expect.Call(folderSelector.SelectFolder()).Return(FolderPath); } using (mockery.Playback()) { model.SetSourcePathCommand.Execute(null); //Due to the sleeps in the stub this should only have 1 item at this stage. Assert.IsTrue(model.Images.Count < fileService.ExpectedFiles.Count); } Thread.Sleep(300); CollectionAssert.AreEquivalent(fileService.ExpectedFiles, model.Images); } private class SlowFileServiceFake : ArtemisWest.Demo.ResponsiveUI._4_MultiThreaded.IFileService { public readonly List<string> ExpectedFiles = new List<string>() { "value0", "value1", "value2", "value3", "value4", "value5" }; public IEnumerable<string> ListFiles(string path, Predicate<string> filter) { foreach (var item in ExpectedFiles) { yield return item; Thread.Sleep(30); } } public IEnumerable<string> ListDirectories(string path) { return EmptyList; } }
Here I have used a Fake to provide canned results with a hard coded delay. In theory this should drip feed values to the presentation model.
I now go back to update my model to get the tests to pass. Following my own advice I change the code to look like the following:
void ExecuteSetSourcePath(object sender, ExecutedEventArgs e) { string folder = this.folderSelector.SelectFolder(); if (!string.IsNullOrEmpty(folder)) { SourcePath = folder; StartLoadingFiles(SourcePath); } } void StartLoadingFiles(string path) { BackgroundWorker fileWorker = new BackgroundWorker(); fileWorker.DoWork += (sender, e) => { LoadPath(path); }; fileWorker.RunWorkerCompleted += (sender, e) => { IsLoading = false; }; fileWorker.RunWorkerAsync(); } void LoadPath(string path) { IEnumerable<string> files = fileService.ListFiles(path, IsImage); foreach (string item in files) { dispatcher.Invoke(new Action(() => { Images.Add(item); })); } IEnumerable<string> directories = fileService.ListDirectories(path); foreach (string item in directories) { LoadPath(item); } }
To my surprise this code does not satisfy the test! The reason is because the Dispatcher is not in an Executing loop. This would normally be started by WPF when the application starts, but here we are just in test code. The solution lies with the DispatcherFrame. A dispatcher frame represents an execution loop in a Dispatcher. We can kick start this loop by simply instantiating a new DispatcherFrame and calling Dispatcher.PushFrame with it as the parameter. See Dan Crevier's blog for a really simple way to unit test in this fashion. If we implement Dan's method we end up with the following test code:
[TestMethod] public void FileService_is_called_Async() { SlowFileServiceFake fileService = new SlowFileServiceFake(); Rhino.Mocks.MockRepository mockery = new Rhino.Mocks.MockRepository(); ArtemisWest.Demo.ResponsiveUI._5_MultiThreadedTest.IFolderSelector folderSelector = mockery.DynamicMock<ArtemisWest.Demo.ResponsiveUI._5_MultiThreadedTest.IFolderSelector>(); PhotoAlbumModel model = new PhotoAlbumModel(Dispatcher.CurrentDispatcher, fileService, folderSelector); using (mockery.Record()) { Expect.Call(folderSelector.SelectFolder()).Return(FolderPath); } DispatcherFrame frame = new DispatcherFrame(); model.PropertyChanged += (sender, e) => { if (e.PropertyName == "IsLoading" && !model.IsLoading) { frame.Continue = false; } }; using (mockery.Playback()) { model.SetSourcePathCommand.Execute(null); //Due to the sleeps in the stub this should only have 1 item at this stage. Assert.IsTrue(model.Images.Count < fileService.ExpectedFiles.Count); Dispatcher.PushFrame(frame); } Thread.Sleep(300); CollectionAssert.AreEquivalent(fileService.ExpectedFiles, model.Images); }
We could call it a day here, however I am not happy enough with this style of coding. My first problem is the explicit sleep I have in there. This makes my test slow. The fastest this test will ever run is 300ms. I could replace that with some sort of loop to check progress but then my test code is looking less like test code and more like low level code to dance-around dispatchers. This raises my other issue; the clutter I have added by adding the DispatcherFrame stuff.
My solution here was to create a construct that allowed me to code my intentions in a way that I thought to be clearer. I wanted to be able to
- test code that would involve multi-threaded code and the dispatcher
- specify what condition defined its completion
- specify a timeout
- treat the code block as a blocking call
The result I came up with is the following:
[TestMethod] public void FileService_is_called_Async() { Rhino.Mocks.MockRepository mockery = new Rhino.Mocks.MockRepository(); SlowFileServiceFake fileService = new SlowFileServiceFake(); ArtemisWest.Demo.ResponsiveUI._5_MultiThreadedTest.IFolderSelector folderSelector = mockery.DynamicMock<ArtemisWest.Demo.ResponsiveUI._5_MultiThreadedTest.IFolderSelector>(); PhotoAlbumModel model = new PhotoAlbumModel(Dispatcher.CurrentDispatcher, fileService, folderSelector); using (mockery.Record()) { Expect.Call(folderSelector.SelectFolder()).Return(FolderPath); } using (DispatcherTester dispatcherTester = new DispatcherTester(Dispatcher.CurrentDispatcher)) { model.PropertyChanged += (sender, e) => { if (e.PropertyName == "IsLoading" && !model.IsLoading) dispatcherTester.Complete(); }; dispatcherTester.Execute(() => { model.SetSourcePathCommand.Execute(null); Assert.IsTrue(model.Images.Count < fileService.ExpectedFiles.Count); }, new TimeSpan(0, 0, 2)); } CollectionAssert.AreEquivalent(fileService.ExpectedFiles, model.Images); }
This code describes a block of code that may require the use of the dispatcher, the condition where the asynchronous code is considered complete and a TimeSpan for a timeout. The test code is still not perfect but I think it is a step in the right direction.
The code for the DispatcherTester looks something like this:
internal sealed class DispatcherTester : IDisposable { private readonly Dispatcher dispatcher; private readonly DispatcherFrame dispatcherFrame = new DispatcherFrame(); public DispatcherTester(Dispatcher dispatcher) { this.dispatcher = dispatcher; } public Dispatcher Dispatcher { get { return dispatcher; } } public void Execute(Action action, TimeSpan timeout) { Execute(action, timeout, new TimeSpan(10)); } public void Execute(Action action, TimeSpan timeout, TimeSpan wait) { Stopwatch stopwatch = Stopwatch.StartNew(); action.Invoke(); Dispatcher.PushFrame(dispatcherFrame); while (dispatcherFrame.Continue && stopwatch.Elapsed < timeout) { Thread.Sleep(wait); } if (stopwatch.Elapsed >= timeout) { dispatcherFrame.Continue = false; Dispatcher.DisableProcessing(); Dispatcher.ExitAllFrames(); throw new TimeoutException(); } } public void Complete() { dispatcherFrame.Continue = false; } #region IDisposable Members public void Dispose() { dispatcherFrame.Continue = false; } #endregion }
If any other developers have hit problems trying to write unit tests for their WPF code then I hope that this snippet of code helps. For those that are interested, the end result for the Model looks like
public class PhotoAlbumModel : INotifyPropertyChanged { #region Fields private readonly Dispatcher dispatcher; private readonly _4_MultiThreaded.IFileService fileService; private readonly IFolderSelector folderSelector; private readonly DelegateCommand setSourcePathCommand; private readonly ObservableCollection<string> images = new ObservableCollection<string>(); private string sourcePath; private bool isLoading = false; #endregion public PhotoAlbumModel(Dispatcher dispatcher, _4_MultiThreaded.IFileService fileService, IFolderSelector folderSelector) { this.dispatcher = dispatcher; this.fileService = fileService; this.folderSelector = folderSelector; setSourcePathCommand = new DelegateCommand(ExecuteSetSourcePath, CanSetSourcePath); this.PropertyChanged += (sender, e) => { if (e.PropertyName == "IsLoading") setSourcePathCommand.OnCanExecuteChanged(); }; } public ObservableCollection<string> Images { get { return images; } } public bool IsLoading { get { return isLoading; } private set { isLoading = value; OnPropertyChanged("IsLoading"); } } public string SourcePath { get { return sourcePath; } private set { sourcePath = value; OnPropertyChanged("SourcePath"); } } public ICommand SetSourcePathCommand { get { return setSourcePathCommand; } } #region Command handlers void CanSetSourcePath(object sender, CanExecuteEventArgs e) { e.CanExecute = !IsLoading; } void ExecuteSetSourcePath(object sender, ExecutedEventArgs e) { string folder = this.folderSelector.SelectFolder(); if (!string.IsNullOrEmpty(folder)) { SourcePath = folder; StartLoadingFiles(SourcePath); } } #endregion #region Private method void StartLoadingFiles(string path) { IsLoading = true; BackgroundWorker fileWorker = new BackgroundWorker(); fileWorker.DoWork += (sender, e) => { LoadPath(path); }; fileWorker.RunWorkerCompleted += (sender, e) => { IsLoading = false; }; fileWorker.RunWorkerAsync(); } void LoadPath(string path) { IEnumerable<string> files = fileService.ListFiles(path, IsImage); foreach (string item in files) { dispatcher.Invoke(new Action(() => { Images.Add(item); })); } IEnumerable<string> directories = fileService.ListDirectories(path); foreach (string item in directories) { LoadPath(item); } } bool IsImage(string file) { string extension = file.ToLower().Substring(file.Length - 4); switch (extension) { case ".bmp": case ".gif": case ".jpg": case ".png": return true; default: return false; } } #endregion #region INotifyPropertyChanged Members public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged(string propertyName) { PropertyChangedEventHandler handler = PropertyChanged; if (handler != null) { handler(this, new PropertyChangedEventArgs(propertyName)); } } #endregion }
Notice that the Folder dialogue code from the previous post has now been pushed out to an interface which cleans up the code and also allows us to test this model.
The data template looks almost identical to the previous post's window xaml:
<DataTemplate DataType="{x:Type local:PhotoAlbumModel}"> <DataTemplate.Resources> <BooleanToVisibilityConverter x:Key="boolToVisConverter"/> </DataTemplate.Resources> <DockPanel> <DockPanel DockPanel.Dock="Top"> <ProgressBar IsIndeterminate="True" Height="18" DockPanel.Dock="Top" Visibility="{Binding Path=IsLoading, Converter={StaticResource boolToVisConverter}}" /> <Button x:Name="SetSourcePath" Command="{Binding Path=SetSourcePathCommand}" DockPanel.Dock="Right">Set Source</Button> <Border BorderThickness="1" BorderBrush="LightBlue" Margin="3"> <Grid> <TextBlock Text="{Binding SourcePath}"> <TextBlock.Style> <Style TargetType="TextBlock"> <Setter Property="Visibility" Value="Visible"/> <Style.Triggers> <DataTrigger Binding="{Binding SourcePath}" Value=""> <Setter Property="Visibility" Value="Collapsed"/> </DataTrigger> <DataTrigger Binding="{Binding SourcePath}" Value="{x:Null}"> <Setter Property="Visibility" Value="Collapsed"/> </DataTrigger> </Style.Triggers> </Style> </TextBlock.Style> </TextBlock> <TextBlock Text="Source not set" Foreground="LightGray" FontStyle="Italic"> <TextBlock.Style> <Style TargetType="TextBlock"> <Setter Property="Visibility" Value="Collapsed"/> <Style.Triggers> <DataTrigger Binding="{Binding SourcePath}" Value=""> <Setter Property="Visibility" Value="Visible"/> </DataTrigger> <DataTrigger Binding="{Binding SourcePath}" Value="{x:Null}"> <Setter Property="Visibility" Value="Visible"/> </DataTrigger> </Style.Triggers> </Style> </TextBlock.Style> </TextBlock> </Grid> </Border> </DockPanel> <ListView ItemsSource="{Binding Images}" ScrollViewer.VerticalScrollBarVisibility="Visible" /> </DockPanel> </DataTemplate>
I hope that the examples here
- give some insight on how you too can unit test your code,
- show why a model is a better option for your WPF applications as you can test them
- give you the courage to write multi threaded code in your WPF models in the knowledge that you can test it
Next we discover that sometimes controls can cause an unresponsive UI in Part 6.
Previous - Responsive WPF User Interfaces Part 4 - Multi-threaded Programming in WPF.
Back to series Table Of Contents
Working version of the code can be found here
4 comments:
Hi Lee,
Have you considered writing an ICommand that performs async operations?
One advantage I can see is you could manage the CanExecute status nicely. It would also simplify testing code.
// possible test code.
// execute the command
presenter.View.MyCommand.Execute(null);
// wait for the command to finish
presenter.View.MyCommand.WaitForComplete();
Just a thought,
James
Nice serie,
Looking forware to chapter 6 and 7.
Regards
Arne
PS: My progressbar is locking - even when it is indeterminate - when changing to reading images (not the string) with many images (even though I use the embedded images) - any suggestions?
Arne,
I am glad you enjoyed the series. The next post addresses the very problem you are facing. It is related to the way the WPF image control resolves its images (on the dispatcher thread!).
Im sorry that I havn't updated the series in some weeks. Have been swampped at work but will try to get something out in the next 2 weeks.
Lee
Stop the press. Thanks to the Rx team there is now an IScheduler interface that effectively negates this post. Check out the Rx introduction on this blog and specifically the scheduling and testing posts
Post a Comment