36

How can one test a method that returns Future before the test runner completes? I have a problem where my unit test runner completes before the asynchronous methods are completed.

Seth Ladd
  • 112,095
  • 66
  • 196
  • 279
adam-singer
  • 4,489
  • 4
  • 21
  • 25

6 Answers6

22

Full example of how to test with the completion matcher is as follows.

import 'package:unittest/unittest.dart';

class Compute {
  Future<Map> sumIt(List<int> data) {
    Completer completer = new Completer();
    int sum = 0;
    data.forEach((i) => sum += i);
    completer.complete({"value" : sum});
    return completer.future;
  }
}

void main() {
  test("testing a future", () {
    Compute compute = new Compute();    
    Future<Map> future = compute.sumIt([1, 2, 3]);
    expect(future, completion(equals({"value" : 6})));
  });
}

The unit test runner might not complete before this code completes. So it would seem that the unit test executed correctly. With Futures that might take longer periods of time to complete the proper way is to utilize completion matcher available in unittest package.

/**
 * Matches a [Future] that completes succesfully with a value that matches
 * [matcher]. Note that this creates an asynchronous expectation. The call to
 * `expect()` that includes this will return immediately and execution will
 * continue. Later, when the future completes, the actual expectation will run.
 *
 * To test that a Future completes with an exception, you can use [throws] and
 * [throwsA].
 */
Matcher completion(matcher) => new _Completes(wrapMatcher(matcher));

One would be tempted to do the following which would be incorrect way of unit testing a returned Future in dart. WARNING: below is an incorrect way to test Futures.

import 'package:unittest/unittest.dart';

class Compute {
  Future<Map> sumIt(List<int> data) {
    Completer completer = new Completer();
    int sum = 0;
    data.forEach((i) => sum+=i);
    completer.complete({"value":sum});
    return completer.future;
  }
}

void main() {
  test("testing a future", () {
    Compute compute = new Compute();
    compute.sumIt([1, 2, 3]).then((Map m) {
      Expect.equals(true, m.containsKey("value"));
      Expect.equals(6, m["value"]);
    });
  });
}
adam-singer
  • 4,489
  • 4
  • 21
  • 25
  • Great ! Perhaps you should move the incorrect code from your answer to your question. – Alexandre Ardhuin Dec 03 '12 at 07:48
  • Oops, I understood that the first code snippet didn't work... But it works. Forget my comment, sorry. – Alexandre Ardhuin Jan 29 '13 at 07:16
  • I replied a little too quickly. I was suggesting to move the first code snipet (which compiles and runs but does not do what is expected) in the question because many SO users copy and paste the first code snippet without reading completely the answer. In that case, they would use an *incorrect way of unit testing a returned Future*. – Alexandre Ardhuin Jan 29 '13 at 07:50
  • Could you explalin why that code is incorrect? There are answers with the same code, maybe it was incorrect at the time? – Michel Feinstein Jan 22 '20 at 19:53
18

As an alternative, here's what I've been doing. It's similar to the answers above:

test('get by keys', () {
  Future future = asyncSetup().then((_) => store.getByKeys(["hello", "dart"]));
  future.then((values) {
    expect(values, hasLength(2));
    expect(values.contains("world"), true);
    expect(values.contains("is fun"), true);
  });
  expect(future, completes);
});

I get a reference to the future, and put all my expect statements inside the then call. Then, I register a expect(future, completes) to ensure it actually completes.

Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
Seth Ladd
  • 112,095
  • 66
  • 196
  • 279
  • I like this example also. I could see an easy way to futures down the chain this way. – adam-singer Feb 07 '13 at 02:45
  • This one is really good, since it also allows you to do things like accessing fields inside futures, so you can get, like, the length of a list in a future – Kira Resari Jan 20 '23 at 08:06
15

Another possibility is to use expectAsync1 function. Working analogue for initial incorrect variant of test would be:

void main() {
  test("testing a future", () {
    Compute compute = new Compute();
    compute.sumIt([1, 2, 3]).then(expectAsync1((Map m) {
      Expect.equals(true, m.containsKey("value"));
      Expect.equals(6, m["value"]);
    }));
  });
}

One advantage in using expectAsync1 for async testing is its composability. Sometimes tests are naturally in need of several sequential async blocks of code. Sample test from mongo_db:

testCursorGetMore(){
  var res;
  Db db = new Db('${DefaultUri}mongo_dart-test');
  DbCollection collection;
  int count = 0;
  Cursor cursor;
  db.open().chain(expectAsync1((c){
    collection = db.collection('new_big_collection2');
    collection.remove();
    return db.getLastError();
  })).chain(expectAsync1((_){
    cursor = new Cursor(db,collection,where.limit(10));
    return cursor.each((v){
     count++;
    });
  })).chain(expectAsync1((dummy){
    expect(count,0);
    List toInsert = new List();
    for (int n=0;n < 1000; n++){
      toInsert.add({"a":n});
    }
    collection.insertAll(toInsert);
    return db.getLastError();
  })).chain(expectAsync1((_){
    cursor = new Cursor(db,collection,where.limit(10));
    return cursor.each((v)=>count++);
  })).then(expectAsync1((v){
    expect(count,1000);
    expect(cursor.cursorId,0);
    expect(cursor.state,Cursor.CLOSED);
    collection.remove();
    db.close();
  }));
}

Update:

Both Future and unittest API's were changed since question was initially asked. Now it is possible just return Future from test function and unittest properly executed it with all async guarded functionality. Combined with fact that chain and then methods of Future are now merged that provide nice syntax for tests with several sequential blocks of code. In current version of mongo_dart same test looks like:

Future testCursorGetMore(){
  var res;
  Db db = new Db('${DefaultUri}mongo_dart-test');
  DbCollection collection;
  int count = 0;
  Cursor cursor;
  return db.open().then((c){
    collection = db.collection('new_big_collection2');
    collection.remove();
    return db.getLastError();
  }).then((_){
    cursor = new Cursor(db,collection,where.limit(10));
    return cursor.forEach((v){
     count++;
    });
  }).then((dummy){
    expect(count,0);
    List toInsert = new List();
    for (int n=0;n < 1000; n++){
      toInsert.add({"a":n});
    }
    collection.insertAll(toInsert);
    return db.getLastError();
  }).then((_){
    cursor = new Cursor(db,collection,null);
    return cursor.forEach((v)=>count++);
  }).then((v){
    expect(count,1000);
    expect(cursor.cursorId,0);
    expect(cursor.state,State.CLOSED);
    collection.remove();
    return db.close();
  });
}
Vadim Tsushko
  • 1,516
  • 9
  • 10
  • ExpectAsync is also useful when you need to test not the future itself but some object property changes – Martynas Jun 25 '14 at 13:46
  • Just returning a future also works for `setUp()` and `tearDown()` when they have to execute some async code so the tests are not executed until the `setUp()` is finished. – Günter Zöchbauer Oct 16 '14 at 09:31
11

3 steps to test methods which return Future:

  1. Make the test async
  2. Instead of expect, use expectLater and await it.
  3. Pass in the future method/getter and wrap the expected value with completion like this :

await expectLater(getSum(2,3), completion(5));

To test the method which computes sum:

Future<int> getSum(int a,int b) async{
  return a+b;
}

We can write test like this :

test("test sum",() async{
  await expectLater(getSum(2,3), completion(5));
});
erluxman
  • 18,155
  • 20
  • 92
  • 126
  • 1
    Thanks a lot. You saved me a lot of hours, even after I spent a lot of hours figuring out what is wrong with my code, not knowing my test is wrong. I run into this problem when I had multiple async calls in a single function. I used to put a 10 microsecond delay in the test, but this is a more elegant solution. – Clement Osei Tano Nov 06 '21 at 20:50
4

See the section on asynchronous tests in this article, or the API documentation for expectAsync.

Below is a brief example. Note that expectAsync() must be called before closure passed to test() returns.

import 'package:unittest/unittest.dart';

checkProgress() => print('Check progress called.');

main() {
  test('Window timeout test', () {
    var callback = expectAsync(checkProgress);
    new Timer(new Duration(milliseconds:100), callback);
  });
}

Another way to wait for a future to complete during a test is to return it from the closure passed to the test function. See this example from the article linked above:

import 'dart:async';
import 'package:unittest/unittest.dart';

void main() {
  test('test that time has passed', () {
    var duration = const Duration(milliseconds: 200);
    var time = new DateTime.now();

    return new Future.delayed(duration).then((_) {
      var delta = new DateTime.now().difference(time);

      expect(delta, greaterThanOrEqualTo(duration));
    });
  });
}
Greg Lowe
  • 15,430
  • 2
  • 30
  • 33
4

For mockito v. 2+ There is possibility to do it with help of

await untilCalled(mockObject.someMethod())
Ilya Sulimanov
  • 7,636
  • 6
  • 47
  • 68