From e5980ecb10f1e30ad807c6cc871ddc9faa009983 Mon Sep 17 00:00:00 2001 From: Tracy Pearson Date: Wed, 31 Aug 2022 23:23:41 -0400 Subject: [PATCH] Easier to read tests? More stable? --- README.md | 30 +++++++ .../MainWindowViewModelTests.cs | 78 +++++++++++++++---- WpfViewModelFirst/MainWindowViewModel.cs | 12 ++- 3 files changed, 97 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 824c188..9d8f981 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,33 @@ # WpfViewModelFirst This project is for researching Actions that need to call an async/await function. + +**Unit Testing:** + +This area has been troubling. The MainWindowViewModel needs to set a value +of the ObservableObject on the UI thread. Doing a straight unit test will +cause a deadlock. + +The first process of testing was learning that the Moq mocking framework +has the ability to trigger the action passed to Part1ViewModel. This has +extra setup and makes the test hard to read. + +The next step was finding a way to wait for the ContinueWith in the method +to finish. That was accomplished with the TaskCompletionSource. +With the code being triggered by the callback, this would exhibit 3 different +behaviors. Many times it would pass. Occassionally it would fail one of the +asserts. The other times it would frustratingly get into a deadlock. + +In searching for a way to test this I learned I can set an attribute on the +class to allow internal methods and fields to be seen by the test assembly. +This helped the test code be easier to read. +[assembly:System.Runtime.CompilerServices.InternalsVisibleTo("WpfViewModelFirst.Tests")] + + +Using a combination of Task.Run and DispatcherFrame the tests appear to +complete successfully every time. + +**Note:** When the Task.Run is awaited the test +will deadlock. + + diff --git a/WpfViewModelFirst.Tests/MainWindowViewModelTests.cs b/WpfViewModelFirst.Tests/MainWindowViewModelTests.cs index 177955d..9acbe67 100644 --- a/WpfViewModelFirst.Tests/MainWindowViewModelTests.cs +++ b/WpfViewModelFirst.Tests/MainWindowViewModelTests.cs @@ -1,4 +1,6 @@ using Moq; +using System.Windows.Controls; +using System.Windows.Threading; using WpfViewModelFirst.Part1; using WpfViewModelFirst.Part2; @@ -17,32 +19,76 @@ namespace WpfViewModelFirst.Tests main.Part1(null); Assert.Equal(part1ViewModel, main.CurrentViewModel); } + //[Fact] + //public async Task Part1CallbackIsSuccessfull() + //{ + // TaskCompletionSource taskCompletionSource = new(); + // Mock mockViewModelFactory = new(); + // mockViewModelFactory.Setup(vmf => vmf.GetPart1ViewModel(It.IsAny>())) + // .Callback>(c => + // { + // Task.Run(() => + // { + // c.Invoke(); + // }); + // }); + // mockViewModelFactory.Setup(vmf => vmf.GetPart2ViewModel()) + // .Returns(() => new Part2ViewModel()); + // MainWindowViewModel main = new(mockViewModelFactory.Object); + // main.ItHappened += () => taskCompletionSource.SetResult(true); + // main.Part1(null); + // await taskCompletionSource.Task.WaitAsync(CancellationToken.None); + // Assert.IsType(main.CurrentViewModel); + // Assert.Single(main.Strings); + //} [Fact] - public async Task Part1CallbackIsSuccessfull() + public async Task Part1ActionWillSetCurrentViewModel() { - TaskCompletionSource taskCompletionSource = new(); Mock mockViewModelFactory = new(); - mockViewModelFactory.Setup(vmf => vmf.GetPart1ViewModel(It.IsAny>())) - .Returns(() => new Part1ViewModel(() => - { - return Task.CompletedTask; - - })) - .Callback>(c => - { - Task.Run(() => - { - c.Invoke(); - }); - }); mockViewModelFactory.Setup(vmf => vmf.GetPart2ViewModel()) .Returns(() => new Part2ViewModel()); MainWindowViewModel main = new(mockViewModelFactory.Object); + TaskCompletionSource taskCompletionSource = new(); main.ItHappened += () => taskCompletionSource.SetResult(true); - main.Part1(null); + + Task.Run(() => + { + DispatcherFrame frame = new(); + frame.Dispatcher.Invoke( + DispatcherPriority.Normal, () => + { + main.Part1Action(); + frame.Continue = true; + }); + Dispatcher.PushFrame(frame); + }); await taskCompletionSource.Task.WaitAsync(CancellationToken.None); Assert.IsType(main.CurrentViewModel); + } + [Fact] + public async Task Part1ActionWillSetSingle() + { + Mock mockViewModelFactory = new(); + mockViewModelFactory.Setup(vmf => vmf.GetPart2ViewModel()) + .Returns(() => new Part2ViewModel()); + MainWindowViewModel main = new(mockViewModelFactory.Object); + TaskCompletionSource taskCompletionSource = new(); + main.ItHappened += () => taskCompletionSource.SetResult(true); + + Task.Run(() => + { + DispatcherFrame frame = new(); + frame.Dispatcher.Invoke( + DispatcherPriority.Normal, () => + { + main.Part1Action(); + frame.Continue = true; + }); + Dispatcher.PushFrame(frame); + }); + await taskCompletionSource.Task.WaitAsync(CancellationToken.None); Assert.Single(main.Strings); } + } } \ No newline at end of file diff --git a/WpfViewModelFirst/MainWindowViewModel.cs b/WpfViewModelFirst/MainWindowViewModel.cs index a509f61..8309a23 100644 --- a/WpfViewModelFirst/MainWindowViewModel.cs +++ b/WpfViewModelFirst/MainWindowViewModel.cs @@ -10,6 +10,7 @@ using System.Windows.Threading; using WpfViewModelFirst.Part1; using WpfViewModelFirst.Part2; +[assembly:System.Runtime.CompilerServices.InternalsVisibleTo("WpfViewModelFirst.Tests")] namespace WpfViewModelFirst { public class MainWindowViewModel : ViewModelBase @@ -29,14 +30,14 @@ namespace WpfViewModelFirst CurrentViewModel = null; CurrentViewModel = _viewModelFactory.GetPart1ViewModel(Part1Action); } - private Task Part1Action() + internal Task Part1Action() { CurrentViewModel = null; - var currentDispatcher = Dispatcher.CurrentDispatcher; + Dispatcher currentDispatcher = Dispatcher.CurrentDispatcher; return LongDelay().ContinueWith((c) => { CurrentViewModel = _viewModelFactory.GetPart2ViewModel(); - currentDispatcher.Invoke(() => + currentDispatcher.Invoke(() => { Strings.Add($"{DateTime.Now}"); }); @@ -49,10 +50,7 @@ namespace WpfViewModelFirst private async Task LongDelay() { // This would be the Repository call in the production application - await Task.Run(async () => - { - await Task.Delay(500); - }); + await Task.Delay(500); return "Completed"; }