Compare commits

...

10 Commits

Author SHA1 Message Date
Tracy Pearson
4bcfdb56b5 Added a comment giving some instructions on how to get internal method visible to this assembly 2022-09-01 09:59:39 -04:00
Tracy Pearson
f19d390b78 Task.Run() leaves a warning it will run synchronosly. 2022-09-01 08:39:24 -04:00
Tracy Pearson
2d8ea50944 Test using the Callback and DispatcherFrame 2022-09-01 00:14:30 -04:00
Tracy Pearson
e5980ecb10 Easier to read tests? More stable? 2022-08-31 23:23:41 -04:00
Tracy Pearson
c36b4581c1 code cleanup 2022-08-30 19:34:41 -04:00
Tracy Pearson
379ec7881f The tests are pasing. The UI is working. The tests sometimes hang. 2022-08-30 13:24:29 -04:00
Tracy Pearson
38df3a3ff3 comment about what the LongDelay is for. 2022-08-30 11:59:51 -04:00
Tracy Pearson
abf09308fc Working with passing tests! 2022-08-30 11:55:08 -04:00
Tracy Pearson
50f0754a0f Corrected a build error 2022-08-30 11:02:37 -04:00
Tracy Pearson
3084a49bdd Using a Func<Task> may be a better option 2022-08-30 10:56:23 -04:00
7 changed files with 151 additions and 19 deletions

View File

@@ -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.

View File

@@ -1,4 +1,6 @@
using Moq;
using System.Windows.Controls;
using System.Windows.Threading;
using WpfViewModelFirst.Part1;
using WpfViewModelFirst.Part2;
@@ -9,26 +11,119 @@ namespace WpfViewModelFirst.Tests
[Fact]
public void Part1WillSetPart1ViewModelToCurrentViewModel()
{
Part1ViewModel part1ViewModel = new(() => _ = true);
Mock<IViewModelFactory> mockViewModelFactory = new Mock<IViewModelFactory>();
mockViewModelFactory.Setup(vmf => vmf.GetPart1ViewModel(It.IsAny<Action>()))
Part1ViewModel part1ViewModel = new(() => { return Task.CompletedTask; });
Mock<IViewModelFactory> mockViewModelFactory = new();
mockViewModelFactory.Setup(vmf => vmf.GetPart1ViewModel(It.IsAny<Func<Task>>()))
.Returns(part1ViewModel);
MainWindowViewModel main = new(mockViewModelFactory.Object);
main.Part1(null);
Assert.Equal(part1ViewModel, main.CurrentViewModel);
}
//[Fact]
//public async Task Part1CallbackIsSuccessfull()
//{
// TaskCompletionSource<bool> taskCompletionSource = new();
// Mock<IViewModelFactory> mockViewModelFactory = new();
// mockViewModelFactory.Setup(vmf => vmf.GetPart1ViewModel(It.IsAny<Func<Task>>()))
// .Callback<Func<Task>>(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<Part2ViewModel>(main.CurrentViewModel);
// Assert.Single(main.Strings);
//}
[Fact]
public void Part1CallbackIsSuccessfull()
public async Task Part1CallBackExample()
{
Mock<IViewModelFactory> mockViewModelFactory = new Mock<IViewModelFactory>();
mockViewModelFactory.Setup(vmf => vmf.GetPart1ViewModel(It.IsAny<Action>()))
.Returns(() => new Part1ViewModel(() => Task.Delay(0)))
.Callback<Action>(c => c.Invoke());
Mock<IViewModelFactory> mockViewModelFactory = new();
mockViewModelFactory.Setup(vmf => vmf.GetPart2ViewModel())
.Returns(() => new Part2ViewModel());
MainWindowViewModel main = new(mockViewModelFactory.Object);
TaskCompletionSource<bool> taskCompletionSource = new();
main.ItHappened += () => taskCompletionSource.SetResult(true);
mockViewModelFactory.Setup(vmf => vmf.GetPart1ViewModel(It.IsAny<Func<Task>>()))
.Callback<Func<Task>>(c =>
{
Task.Run(() =>
{
DispatcherFrame frame = new();
frame.Dispatcher.Invoke(
DispatcherPriority.Normal, () =>
{
c.Invoke();
frame.Continue = true;
});
Dispatcher.PushFrame(frame);
});
});
main.Part1(null);
await taskCompletionSource.Task.WaitAsync(CancellationToken.None);
Assert.IsType<Part2ViewModel>(main.CurrentViewModel);
Assert.Single(main.Strings);
}
// This method requires the Part1Action method to be internal not private
// The MainWindowViewModel class also needs to have an attribute allowing
// this assembly to see the internals
// [assembly:System.Runtime.CompilerServices.InternalsVisibleTo("WpfViewModelFirst.Tests")]
[Fact]
public async Task Part1ActionWillSetCurrentViewModel()
{
Mock<IViewModelFactory> mockViewModelFactory = new();
mockViewModelFactory.Setup(vmf => vmf.GetPart2ViewModel())
.Returns(() => new Part2ViewModel());
MainWindowViewModel main = new(mockViewModelFactory.Object);
TaskCompletionSource<bool> taskCompletionSource = new();
main.ItHappened += () => taskCompletionSource.SetResult(true);
Task run = new(() =>
{
DispatcherFrame frame = new();
frame.Dispatcher.Invoke(
DispatcherPriority.Normal, () =>
{
main.Part1Action();
frame.Continue = true;
});
Dispatcher.PushFrame(frame);
});
run.Start();
await taskCompletionSource.Task.WaitAsync(CancellationToken.None);
Assert.IsType<Part2ViewModel>(main.CurrentViewModel);
}
[Fact]
public async Task Part1ActionWillSetSingle()
{
Mock<IViewModelFactory> mockViewModelFactory = new();
mockViewModelFactory.Setup(vmf => vmf.GetPart2ViewModel())
.Returns(() => new Part2ViewModel());
MainWindowViewModel main = new(mockViewModelFactory.Object);
TaskCompletionSource<bool> taskCompletionSource = new();
main.ItHappened += () => taskCompletionSource.SetResult(true);
Task run = new(() =>
{
DispatcherFrame frame = new();
frame.Dispatcher.Invoke(
DispatcherPriority.Normal, () =>
{
main.Part1Action();
frame.Continue = true;
});
Dispatcher.PushFrame(frame);
});
run.Start();
await taskCompletionSource.Task.WaitAsync(CancellationToken.None);
Assert.Single(main.Strings);
}
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Threading.Tasks;
using WpfViewModelFirst.Part1;
using WpfViewModelFirst.Part2;
@@ -6,7 +7,7 @@ namespace WpfViewModelFirst
{
public interface IViewModelFactory
{
Part1ViewModel GetPart1ViewModel(Action finishAction);
Part1ViewModel GetPart1ViewModel(Func<Task> finishAction);
Part2ViewModel GetPart2ViewModel();
}
}

View File

@@ -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
@@ -20,7 +21,7 @@ namespace WpfViewModelFirst
StartCommand = new CustomCommand(Part1);
}
private IViewModelFactory _viewModelFactory;
private readonly IViewModelFactory _viewModelFactory;
public ICommand StartCommand { get; set; }
@@ -29,24 +30,26 @@ namespace WpfViewModelFirst
CurrentViewModel = null;
CurrentViewModel = _viewModelFactory.GetPart1ViewModel(Part1Action);
}
private void Part1Action()
internal Task Part1Action()
{
var currentDispatcher = Dispatcher.CurrentDispatcher;
CurrentViewModel = null;
LongDelay().ContinueWith(c =>
Dispatcher currentDispatcher = Dispatcher.CurrentDispatcher;
return LongDelay().ContinueWith((c) =>
{
System.Diagnostics.Debug.WriteLine(c.Result);
CurrentViewModel = _viewModelFactory.GetPart2ViewModel();
currentDispatcher.Invoke(() =>
{
Strings.Add($"{DateTime.Now}");
}
);
});
ItHappened?.Invoke();
}, TaskContinuationOptions.OnlyOnRanToCompletion);
}
public event Action? ItHappened;
private async Task<string> LongDelay()
{
// This would be the Repository call in the production application
await Task.Delay(500);
return "Completed";
}

View File

@@ -12,6 +12,9 @@
<RowDefinition/>
</Grid.RowDefinitions>
<Button Grid.Row="0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Padding="10"
Content="Finish"
Command="{Binding FinishCommand}"/>
</Grid>

View File

@@ -9,9 +9,9 @@ namespace WpfViewModelFirst.Part1
{
public class Part1ViewModel : ViewModelBase
{
private readonly Action _finishAction;
private readonly Func<Task> _finishAction;
public Part1ViewModel(Action finishAction)
public Part1ViewModel(Func<Task> finishAction)
{
_finishAction = finishAction;
FinishCommand = new CustomCommand(Finish);

View File

@@ -8,7 +8,7 @@ namespace WpfViewModelFirst
{
public class ViewModelFactory : IViewModelFactory
{
public Part1.Part1ViewModel GetPart1ViewModel(Action finishAction)
public Part1.Part1ViewModel GetPart1ViewModel(Func<Task> finishAction)
{
return new(finishAction);
}