Easier to read tests? More stable?

This commit is contained in:
Tracy Pearson
2022-08-31 23:23:41 -04:00
parent c36b4581c1
commit e5980ecb10
3 changed files with 97 additions and 23 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;
@@ -17,32 +19,76 @@ namespace WpfViewModelFirst.Tests
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 async Task Part1CallbackIsSuccessfull()
public async Task Part1ActionWillSetCurrentViewModel()
{
TaskCompletionSource<bool> taskCompletionSource = new();
Mock<IViewModelFactory> mockViewModelFactory = new();
mockViewModelFactory.Setup(vmf => vmf.GetPart1ViewModel(It.IsAny<Func<Task>>()))
.Returns(() => new Part1ViewModel(() =>
{
return Task.CompletedTask;
}))
.Callback<Func<Task>>(c =>
{
Task.Run(() =>
{
c.Invoke();
});
});
mockViewModelFactory.Setup(vmf => vmf.GetPart2ViewModel())
.Returns(() => new Part2ViewModel());
MainWindowViewModel main = new(mockViewModelFactory.Object);
TaskCompletionSource<bool> 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<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(() =>
{
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);
}
}
}

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
@@ -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<string> 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";
}