Reputation: 107
I have a viewModel with async Task. I don't now how to test it.
public class MyViewModel : BindableBase
{
public MyViewModel()
{
this.PropertyChanged += MyViewModel_PropertyChanged;
}
private void MyViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
Action action = async () => await DoSomething();
action();
}
public const string BeforeKey = "before";
public const string AfterKey = "After";
public string Status { get; private set; } = BeforeKey;
public async Task DoSomething()
{
await Task.Delay(3000);
Status = AfterKey;
}
string bindagleProp;
public string BindagleProp
{
get { return bindagleProp; }
set { SetProperty(ref bindagleProp, value); }
}
}
Here is my test:
[TestMethod]
public async Task TestMyViewModel()
{
MyViewModel viewModel = new MyViewModel();
Assert.AreEqual(viewModel.Status, MyViewModel.BeforeKey, "before check");
viewModel.BindagleProp = "abc";
Assert.AreEqual(viewModel.Status, MyViewModel.AfterKey, "after check");
}
The test failed because it's not waiting to completion of the task.
I DON'T want to use Task.Delay in the unit test, because it's not safety. DoSomething
method can has unknown duration time.
Thank you for any help.
Edit:
In fact, The issue is not specific for MVVM, but for any async event handler. For example:
// class with some logic, can be UI or whatever.
public class MyClassA
{
Size size;
public Size Size
{
get { return size; }
set
{
size = value;
SizeChanged?.Invoke(this, EventArgs.Empty);
}
}
public event EventHandler SizeChanged;
}
// this class uses the MyClassA class.
public class MyCunsomerClass
{
readonly MyClassA myClassA = new MyClassA();
public MyCunsomerClass()
{
myClassA.SizeChanged += MyClassA_SizeChanged;
}
public string Status { get; private set; } = "BEFORE";
private async void MyClassA_SizeChanged(object sender, EventArgs e)
{
await LongRunningTaskAsync();
Status = "AFTER";
}
public async Task LongRunningTaskAsync()
{
await Task.Delay(3000);
///await XYZ....;
}
public void SetSize()
{
myClassA.Size = new Size(20, 30);
}
}
Now, I want to test it:
[TestMethod]
public void TestMyClass()
{
var cunsomerClass = new MyCunsomerClass();
cunsomerClass.SetSize();
Assert.AreEqual(cunsomerClass.Status, "AFTER");
}
The test failed.
Upvotes: 4
Views: 736
Reputation: 107
I asked Stehphen Cleary [The famous professor of asynchronous], and he answered me:
If by "async event handler" you mean an
async void
event handler, then no, those aren't testable. However, they are often useful in a UI application. So what I usually end up doing is having all my async void methods be exactly one line long. They all look like this:
async void SomeEventHandler(object sender, EventArgsOrWhatever args)
{
await SomeEventHandlerAsync(sender, args);
}
async Task SomeEventHandlerAsync(object sender, EventArgsOrWhatever args)
{
... // Actual handling logic
}
Then the
async Task
version is unit testable, composable, etc. Theasync void
handler isn't, but that's acceptable since it no longer has any real logic at all.
Thanks Stephen! Your idea is excellent!
Upvotes: 3
Reputation: 3375
Ok So first of all, I would move the worker out to an other class and make an interface to it. So that when I run the test I can inject another worker!
public class MyViewModel : BindableBase
{
private IWorker _worker;
private readonly DataHolder _data = new DataHolder(){Test = DataHolder.BeforeKey};
public string Status { get { return _data.Status; } }
public MyViewModel(IWorker worker = null)
{
_worker = worker;
if (_worker == null)
{
_worker = new Worker();
}
this.PropertyChanged += MyViewModel_PropertyChanged;
}
private void MyViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
Action action = async () => await _worker.DoSomething(_data);
action();
}
string bindagleProp;
public string BindagleProp
{
get { return bindagleProp; }
set { SetProperty(ref bindagleProp, value); }
}
}
public class DataHolder
{
public const string BeforeKey = "before";
public const string AfterKey = "After";
public string Status;
}
public interface IWorker
{
Task DoSomething(DataHolder data);
}
public class Worker : IWorker
{
public async Task DoSomething(DataHolder data)
{
await Task.Delay(3000);
data.Status = DataHolder.AfterKey;
}
}
Now the inject code would look something like:
[TestMethod]
public async Task TestMyViewModel()
{
TestWorker w = new TestWorker();
MyViewModel viewModel = new MyViewModel(w);
Assert.AreEqual(viewModel.Status, DataHolder.BeforeKey, "before check");
viewModel.BindagleProp = "abc";
Assert.AreEqual(viewModel.Status, DataHolder.AfterKey, "after check");
}
public class TestWorker : IWorker
{
public Task DoSomething(DataHolder data)
{
data.Status = DataHolder.BeforeKey;
return null; //you maybe should return something else here...
}
}
Upvotes: 2