Home > Back-end >  Dart Unit Tests - Await Results of an Async Function in Class Constructor
Dart Unit Tests - Await Results of an Async Function in Class Constructor

Time:08-17

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 being extended.

  • 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 and mainFoo by declaring mainFoo as a Future<Foo?> directly.

(In the above code, I assumed that loadMainFoo is meant to have a return type of Future<Foo?> insead of Future<Foo>?.)

  • Related