如何在Dart中测试Future?

36

在测试运行器完成之前如何测试返回 Future 的方法?我的问题是单元测试运行器在异步方法完成之前就已经完成了。

6个回答

22

如何使用completion匹配器进行测试的完整示例如下。

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})));
  });
}

在这段代码完成之前,单元测试运行器可能无法完成。因此,看起来单元测试执行正确。对于需要更长时间才能完成的Future,正确的方法是利用unittest包中提供的completion匹配器。

/**
 * 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));

以下是一种错误的在Dart中对返回的Future进行单元测试的方法,会引起人们的诱惑。 警告:以下是测试Futures的一种错误方式。

以下是一种错误的在Dart中对返回的Future进行单元测试的方法,会引起人们的诱惑。 警告:以下是测试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"]);
    });
  });
}

太好了!也许你应该把错误的代码从你的回答中移到问题里。 - Alexandre Ardhuin
糟糕,我误解了第一个代码片段不起作用...但它确实有效。忘记我的评论,抱歉。 - Alexandre Ardhuin
我回复得有点太快了。我建议将问题中第一个代码片段(它可以编译和运行,但不符合预期)移动,因为许多SO用户会复制并粘贴第一个代码片段而没有完全阅读答案。在这种情况下,他们会使用一种错误的方式来对返回的Future进行单元测试 - Alexandre Ardhuin
你能解释一下为什么那段代码是错误的吗?可能有相同的代码答案,但在当时可能是错误的吗? - Michel Feinstein

18

作为一种替代方案,这是我一直在做的。它与上面的答案类似:

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);
});

我获取了一个对未来的引用,并将所有期望语句放在then调用内。然后,我注册expect(future, completes)以确保它实际上完成。

我获取一个Future对象的引用,并将所有期望的语句放在then函数中。接着,我使用expect(future, completes)注册一个完成的回调函数来确保该Future对象会成功完成。


我也喜欢这个例子。我可以看到通过这种方式轻松地将未来的事情逐步实现。 - adam-singer
这个非常好,因为它还允许您做一些事情,比如访问未来内部的字段,这样您就可以获取未来列表的长度。 - Kira Resari

15

另一个可能性是使用expectAsync1函数。对于测试的初始不正确变体,可以使用工作类似的方式:

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"]);
    }));
  });
}

使用expectAsync1进行异步测试的一个优点是它的可组合性。有时测试自然需要几个顺序执行的异步代码块。

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();
  }));
}

更新:

自问题最初提出以来,Futureunittest API都发生了变化。现在可以从测试函数中返回 Future,并且unittest会正确执行所有异步保护功能。与Futurechainthen方法现已合并,为具有多个连续代码块的测试提供了漂亮的语法。在当前版本的mongo_dart中,同样的测试看起来像:

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();
  });
}

ExpectAsync 在需要测试对象属性变化而非未来本身时也很有用。 - Martynas
setUp()tearDown()需要执行一些异步代码以便测试在setUp()完成之前不被执行时,只需返回一个future即可。 - Günter Zöchbauer

11

测试返回Future的方法有三个步骤:

  1. 将测试设置为异步
  2. 使用expectLater代替expect,并使用await等待。
  3. 传入Future方法/获取器并使用completion包装期望值,如下所示:

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

要测试计算总和的方法:

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

我们可以这样编写测试:

test("test sum",() async{
  await expectLater(getSum(2,3), completion(5));
});

1
非常感谢。您为我节省了很多时间,即使在我花费了很多时间来找出我的代码有什么问题时,也不知道我的测试有问题。当我在单个函数中有多个异步调用时,我遇到了这个问题。我以前会在测试中放置一个10微秒的延迟,但这是一个更优雅的解决方案。 - Clement Osei Tano

4

对于 mockito 版本 2+,有一种方法可以帮助您实现此操作。

await untilCalled(mockObject.someMethod())

4
请参考该文章中异步测试的部分,或者查看expectAsync的API文档。以下是一个简短的例子。请注意,在传递给test()的闭包返回之前,必须先调用expectAsync()。
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);
  });
}

在测试过程中等待未来完成的另一种方法是从传递给测试函数的闭包中返回它。请参阅上面链接文章中的示例:

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));
    });
  });
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接