I have a FooViewModel that is supposed to check Cloud Firestore for a mainFoo upon initialization, and set a mainFoo property to this value if one is found.
Since it's looking at cloud Firestore, it's an asynchronous function.
The view is supposed to show an empty value view if no mainFoo exists, and a view showing the mainFoo if one does exist.
This works fine in practice, but where I'm struggling is my unit testing.
I want to test that this method is called when the viewModel is initialized, and that it does set the mainFoo if one does exist. But setting my result to mainFoo seems to happen before loadMainFoo has completed, and result still ends up as null.
Here's the basics of what's in the class:
class FooViewModel extends BaseViewModel {
// MARK
// Properties
Foo? mainFoo;
FooViewModel() {
loadMainFoo();
}
/// Returns the mainFoo if there is one.
Future<Foo>? loadMainFoo() async {
// Code in here to load the Foo from Firestone, and sets the mainFoo to the Foo that gets loaded.
}
}
My test sets sut to a new instance of FooViewModel, which will also trigger loadMainFoo. The next line is to set my result to the mainFoo property in sut.
This is where the issue seems to happen - I know that loadMainFoo is being called, but result is getting set before loadMainFoo has finished, and it still ends up as null.
Is there any way to make sure result doesn't get set until I can be sure loadMainFoo has completed?
As far as I know I can't make the constructors asynchronous.
test('Makes sure mainFoo exists after logic performed', () async {
// ARRANGE
sut = FooViewModel();
// ACT
var result = sut.mainFoo;
// ASSERT
expect(result != null, true);
});
CodePudding user response:
As far as I know I can't make the constructors asynchronous.
Correct. Constructors must return an instance of the constructed class without exception. Constructors therefore cannot return a Future, and therefore if a constructor does any asynchronous work, callers cannot be directly notified when that asynchronous work completes.
If your constructor must do asynchronous work, you instead can:
Make the constructor private and use a
staticmethod as a factory method:class FooViewModel extends BaseViewModel { Foo mainFoo; FooViewModel._(this.mainFoo); Future<Foo?> loadMainFoo() async => FooViewModel._(await someAsynchronousOperation()); }This is the most straightforward for callers. However, this would prevent
FooViewModelfrom beingextended.Expose the
Futurefor asynchronous work as a property on the constructed object:class FooViewModel extends BaseViewModel { Foo? mainFoo; late final Future<void> initialized; FooViewModel.() { initialized = loadMainFoo(); } Future<Foo?> loadMainFoo() async { ... } }and then callers can do:
var sut = FooViewModel(); await sut.initialized;This is more work for callers and therefore is inherently more error-prone. Also consider combining
initializedandmainFooby declaringmainFooas aFuture<Foo?>directly.
(In the above code, I assumed that loadMainFoo is meant to have a return type of Future<Foo?> insead of Future<Foo>?.)
