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
static
method 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
FooViewModel
from beingextend
ed.Expose the
Future
for 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
initialized
andmainFoo
by declaringmainFoo
as 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>?
.)