「这是我参与2022首次更文挑战的第19天,活动详情查看:2022首次更文挑战」。
本文翻译自pub: mockito | Dart Package (flutter-io.cn)
译时版本: mockito 5.0.17
mockito
Dart 的 Mock 库,受 Mockito 启发。
我们来创建 mock
Mockito 5.0.0 支持 Dart2.12 的 非空 语言特性,主要是代码生成。
要使用 Mockito 生成的 Mock 类,在 pubspec.yaml
文件中的 dev_dependencies
下添加 build_runner
依赖。如 build_runner: ^1.11.0
之类的包。
作为代码生成 API 的替代方案,查看 NULL_SAFETY_README。
让我们以一个 Dart 库开始, cat.dart
:
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'cat.mocks.dart';
// 真实的类
class Cat {
String sound() => "Meow";
bool eatFood(String food, {bool? hungry}) => true;
Future<void> chew() async => print("Chewing...");
int walk(List<String> places) => 7;
void sleep() {}
void hunt(String place, String prey) {}
int lives = 9;
}
// 注解用于创建 cat.mocks.dart 库和 MockCat 类。
@GenerateMocks([Cat])
void main() {
// 创建 mock 对象
var cat = MockCat();
}
通过使用 @GenerateMocks
注解库元素(例如,测试文件的 main
函数、或类),可以指示 Mockito 的代码生成器为每个列出的真正类在一个新的库里生成一个 mock 类。
下一步是运行 build_runner
以生成这个新的库。
dart run build_runner build
build_runner
会基于包含 @GenerateMocks
注解的文件名生成一个文件。在上面的 cat.dart
示例中,我们导入了生成的库作为 cat.mocks.dart
。
生成的 mock 类,MockCat
,继承了 Mockito 的 Mock 类并实现了 Cat
类, 给予我们一个支持插桩和验证的类。
让我们验证一些行为
// 使用 mock 对象交互
cat.sound();
// 验证交互
verify(cat.sound());
一旦创建, mock 实例会记住所有的交互。然后你可以选择性地 verify
(验证)(或) verifyInOrder
(按顺序验证) (或) verifyNever
) (永不验证))感兴趣的交互。
一些插桩如何?
// 在交互前插桩一个 mock 方法
when(cat.sound()).thenReturn("Purr");
expect(cat.sound(), "Purr");
// 可以再次调用
expect(cat.sound(), "Purr");
// 让我们改变一下插桩
when(cat.sound()).thenReturn("Meow");
expect(cat.sound(), "Meow");
// 可以对 getter 插桩
when(cat.lives).thenReturn(9);
expect(cat.lives, 9);
// 可以插桩一个方法用来 throw
when(cat.lives).thenThrow(RangeError('Boo'));
expect(() => cat.lives, throwsRangeError);
// 可以在调用时处理响应
var responses = ["Purr", "Meow"];
when(cat.sound()).thenAnswer((_) => responses.removeAt(0));
expect(cat.sound(), "Purr");
expect(cat.sound(), "Meow");
when
、 thenReturn
、 thenAnswer
和 thenThrow
API 提供了一个覆写该行为的插桩机制。一旦插桩,无论调用多少次,方法会总是返回插桩的值。
如果一个方法调用匹配多个插桩,最后一个被声明的会被使用。
只对 mock 类的方法进行插桩和验证没有意义;在本例中, MockCat
的一个实例会被使用,而不是 Cat
的一个实例。
异步插桩的简单说明
使用 thenReturn
返回 Future
或 Stream
会抛出一个 ArgumentError
。 这是因为它会导致不可预知的行为。例如:
- 如果方法在一个不同的区域(并非消费
Future
)的区域被插桩,就会发生不可预知的行为。 - 如果方法被插桩用于返回一个失败的
Future
或Stream
,并且它没有在相同的运行循环中被消费,那它有可能会被全局的异常处理器消费,而不是应由它消费的异常处理器。
取而代之,使用 thenAnswer
来插桩方法返回一个 Future
或 Stream
。
// BAD
when(mock.methodThatReturnsAFuture())
.thenReturn(Future.value('Stub'));
when(mock.methodThatReturnsAStream())
.thenReturn(Stream.fromIterable(['Stub']));
// GOOD
when(mock.methodThatReturnsAFuture())
.thenAnswer((_) async => 'Stub');
when(mock.methodThatReturnsAStream())
.thenAnswer((_) => Stream.fromIterable(['Stub']));
如果,由于一些原因,你期望 thenReturn
的行为,可以返回一个预定义的实例。
// 使用上面的方法,除非能确定要事先创建 Future 。
final future = Future.value('Stub');
when(mock.methodThatReturnsAFuture()).thenAnswer((_) => future);
Argument matchers
Mockito 提供了 “参数匹配器”(使用 ArgMatcher 类)的概念来捕获参数,并且追踪命名的参数如何传递。大多数情况下,普通参数和参数匹配器可以传递到 mock 方法中:
// 它们自身可以使用 普通参数
when(cat.eatFood("fish")).thenReturn(true);
// ... 包含集合
when(cat.walk(["roof","tree"])).thenReturn(2);
// ... 或匹配器
when(cat.eatFood(argThat(startsWith("dry")))).thenReturn(false);
when(cat.eatFood(any)).thenReturn(false);
// ... 或带匹配器的混合参数
when(cat.eatFood(argThat(startsWith("dry")), hungry: true)).thenReturn(true);
expect(cat.eatFood("fish"), isTrue);
expect(cat.walk(["roof","tree"]), equals(2));
expect(cat.eatFood("dry food"), isFalse);
expect(cat.eatFood("dry food", hungry: true), isTrue);
// 也可以验证参数匹配器的使用
verify(cat.eatFood("fish"));
verify(cat.walk(["roof","tree"]));
verify(cat.eatFood(argThat(contains("food"))));
// 可以验证 setter
cat.lives = 9;
verify(cat.lives=9);
如果一个参数不是 ArgMatcher, (如 any
、 anyNamed
、 argThat
、 captureThat
、等) 传递给 mock 方法的参数,那么 equals
匹配器用于参数匹配。 如果需要更严格的匹配,考虑下使用 argThat(identical(arg))
。
尽管这样,注意 null
不能用作 ArgMatcher 参数相邻的参数,或传递一个未包装的值作为命名参数。例如:
verify(cat.hunt("backyard", null)); // OK:没有参数匹配器
verify(cat.hunt(argThat(contains("yard")), null)); // BAD:相邻参数为 null
verify(cat.hunt(argThat(contains("yard")), argThat(isNull))); // OK:使用参数匹配器包装
verify(cat.eatFood("Milk", hungry: null)); // BAD:使用 null 作为命名参数
verify(cat.eatFood("Milk", hungry: argThat(isNull))); // BAD:使用 null 作为命名参数
命名参数
关于此语法,Mockito 现在有一个尴尬的麻烦:命名参数和参数匹配器需要比想象中更多的配置:必须在参数匹配器中声明参数的名称。这是因为我们无法依赖命名参数的位置,并且语言没有提供一个机制来回答 ”这个元素是在用作命名元素” 吗?
// GOOD: 参数匹配器包含它们的名称。
when(cat.eatFood(any, hungry: anyNamed('hungry'))).thenReturn(true);
when(cat.eatFood(any, hungry: argThat(isNotNull, named: 'hungry'))).thenReturn(false);
when(cat.eatFood(any, hungry: captureAnyNamed('hungry'))).thenReturn(false);
when(cat.eatFood(any, hungry: captureThat(isNotNull, named: 'hungry'))).thenReturn(true);
// BAD: 参数匹配器不包含它们的名称。
when(cat.eatFood(any, hungry: any)).thenReturn(true);
when(cat.eatFood(any, hungry: argThat(isNotNull))).thenReturn(false);
when(cat.eatFood(any, hungry: captureAny)).thenReturn(false);
when(cat.eatFood(any, hungry: captureThat(isNotNull))).thenReturn(true);
验证精确的调用次数 / 至少 x 次 / 永不
使用 verify
或者 verifyNever
:
cat.sound();
cat.sound();
// 精确的调用次数
verify(cat.sound()).called(2);
// 或使用匹配器
verify(cat.sound()).called(greaterThan(1));
// 或永不调用
verifyNever(cat.eatFood(any));
按顺序验证
使用 verifyInOrder
:
cat.eatFood("Milk");
cat.sound();
cat.eatFood("Fish");
verifyInOrder([
cat.eatFood("Milk"),
cat.sound(),
cat.eatFood("Fish")
]);
按顺序验证是弹性的 - 不需要一个接一个地验证交互,而是只按顺序验证感兴趣的交互。
确认交互在 mock 中从未发生
verifyZeroInteractions(cat);
发现冗余调用
cat.sound();
verify(cat.sound());
verifyNoMoreInteractions(cat);
为之后的断言捕获参数
使用 captureAny
、 captureThat
、 和 captureAnyNamed
参数匹配器:
// 简单捕获
cat.eatFood("Fish");
expect(verify(cat.eatFood(captureAny)).captured.single, "Fish");
// 捕获多个调用
cat.eatFood("Milk");
cat.eatFood("Fish");
expect(verify(cat.eatFood(captureAny)).captured, ["Milk", "Fish"]);
// 条件捕获
cat.eatFood("Milk");
cat.eatFood("Fish");
expect(verify(cat.eatFood(captureThat(startsWith("F")))).captured, ["Fish"]);
等待交互
使用 untilCalled
:
// 等待调用
cat.eatFood("Fish");
await untilCalled(cat.chew()); // 当 cat.chew() 被调用时完成
// 等待已经发生的调用
cat.eatFood("Fish");
await untilCalled(cat.eatFood(any)); // 当即完成
编写 fake
也可以通过继承Fake编写一个简单的假类来实现真实的类。Fake可以使子类满足真实类的实现,而无需覆写在测试中没有用到的方法。 Fake 类实现了抛出UnimplementedError 的默认行为(可以在假的类中覆写):
// 假的类
class FakeCat extends Fake implements Cat {
@override
bool eatFood(String food, {bool? hungry}) {
print('Fake eat $food');
return true;
}
}
void main() {
// 在运行时创建一个新的假 Cat(实例)。
var cat = FakeCat();
cat.eatFood("Milk"); // 打印 'Fake eat Milk'。
cat.sleep(); // 抛出
}
重置 mock
使用 reset
:
// 清空集成的所有交互:
cat.eatFood("Fish");
clearInteractions(cat);
cat.eatFood("Fish");
verify(cat.eatFood("Fish")).called(1);
// 重置插桩和集成的交互:
when(cat.eatFood("Fish")).thenReturn(true);
cat.eatFood("Fish");
reset(cat);
when(cat.eatFood(any)).thenReturn(false);
expect(cat.eatFood("Fish"), false);
调试
使用 logInvocations
和 throwOnMissingStub
:
// 打印 mock 对象列表的任意 mock 方法的所有集成的交互:
logInvocations([catOne, catTwo]);
// 如果调用的 mock 方法没有一个匹配的插桩,每次都会抛出异常:
throwOnMissingStub(cat);
最佳实践
相对于使用 mock 测试,使用真实的对象测试是优先的 - 如果可以为测试构造一个真实的实例的话,就该这样去做。
如果在测试中没有调用 verify
,
这是完全不需要 mock 的强烈信号,即使使用如插桩的 Mock
也没什么问题。
当无法使用真实的对象时,使用假类的可测试实现是第二选择 - 相对于在测试中的真实响应,它几乎可以有和真实的类一样的行为。最后,继承 Fake
的对象使用手动覆写的方法,优先于
继承 Mock
的对象作为插桩或 mock 使用。
继承 Mock
的类 决不 应该在构造方法中或其它地方使用 when
插桩它自身的响应。
在测试中插桩的响应应该在它使用的地方定义。
因为无论是对于整个接口,或者使用 继承 Fake
跳过接口的一些部分,响应的控制都是在使用 @override
方法的测试之外。
类似地,继承 Mock
的类 决不 就该有任何 @override
方法。
它们无法被测试插桩或者被 Mockito 追踪和验证。
同时定义了插桩响应的测试和定义了覆写的 mock 的混合会导致混乱。
如果有助于代码结构,可以对 继承 Mock
的类定义 静态的 工具(类或方法)。
常见问题
更多关于该包的信息,请阅读 FAQ 。