I'm following Reso Coder's video about unit tests in Flutter / Dart. In the video, he is testing a class that makes an async call to a remote service (mocked using Mockito):
Class being tested (not the full code):
Class ServiceChangeNotifier extends ChangeNotifier {
final Service _service;
ServiceChangeNotifier(Service this._service);
bool _isLoading = false;
bool get isLoading => _isLoading;
List<int> _res = [];
List<int> get res => _res;
Future<void> doService() async {
_isLoading = true;
notifyListeners();
_res = await _service.runService();
_isLoading = false;
notifyListeners();
}
Notice he's changing _isLoading
to true before the service call, and then to false after.
Now, in the test, he wants to verify that the loading state gets updated correctly to true before the service call, and then to false after the service. To do so, he is setting the call to the async service method into a Future variable, then asserts the true change, then awaits on the future variable, and then asserts for false:
Unit test code
final ServiceChangeNotifier sut = ServiceChangeNotifier(serviceMockup);
test('....',
() async {
// test setup...
final future = sut.doService() // 1
expect(sut.isLoading, true); // 2
await future; // 3
expect(sut.isLoading, false); // 4
// ...
}
I understand why you can await the method before testing for true, because it will change to false before the expect(true) gets to run. What I don't understand is how the check for true (line 2 above) works. Is the logic of line 1 (future assignment) the one that triggers the execution of the doService method, or is it the await in line 3?
I suspect I'm missing something basic about how Future/await works and/or how expect() works in the test library. This question is somewhat related to this question, but here I'm not asking about the async part of the doSerivce method, but rather how the synchronous assignments to _isLoading is being evaluated in light of the asynchronous call.
CodePudding user response:
When you execute an asynchronous function, as much of it as possible is executed synchronously; that is, its function body is executed until it reaches an await
(which is syntactic sugar for returning a Future
and registering appropriate completion callbacks).
So given:
final future = sut.doService() // 1 expect(sut.isLoading, true); // 2 await future; // 3 expect(sut.isLoading, false); // 3
sut.doService()
is invoked immediately.doService
's body immediately sets_isLoading = true
, invokesnotfifyListeners()
[sic], invokes_service.runService()
, and then returns aFuture
.sut.isLoading
at this point istrue
.- The
Future
returned by step 1 isawait
ed, so execution returns to the event loop. TheFuture
from_service.runService()
eventually completes, which executes the rest ofdoService()
's body, setting_isLoading = false
and invokingnotfifyListeners()
again. sut.isLoading
at this point isfalse
.