Flutter单元测试模拟插件mockito的使用
Flutter单元测试模拟插件mockito的使用
Mockito 是一个用于 Dart 的模拟库,它允许我们创建模拟对象来替代真实对象进行单元测试。这在测试过程中非常有用,特别是当我们想要隔离被测代码与外部依赖(如网络请求、数据库等)时。
创建模拟对象
从 Mockito 5.0.0 开始,它支持 Dart 的新特性——空安全,并主要通过代码生成来实现。为了使用 Mockito 的生成的模拟类,在 pubspec.yaml
文件中添加 build_runner
依赖:
dev_dependencies:
build_runner: ^2.3.3
然后,在你的 Dart 文件中使用 @GenerateNiceMocks
注解来生成模拟类:
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
// Annotation which generates the cat.mocks.dart library and the MockCat class.
@GenerateNiceMocks([MockSpec<Cat>()])
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;
}
void main() {
// Create mock object.
var cat = MockCat();
}
接下来,运行 build_runner
来生成 .mocks.dart
文件:
flutter pub run build_runner build
# OR
dart run build_runner build
验证行为
一旦创建了模拟对象,就可以验证它的交互行为:
cat.sound();
verify(cat.sound());
模拟方法
你可以用 when
方法来模拟返回值或异常:
when(cat.sound()).thenReturn("Purr");
expect(cat.sound(), "Purr");
when(cat.lives).thenThrow(RangeError('Boo'));
expect(() => cat.lives, throwsRangeError);
对于异步方法,推荐使用 thenAnswer
:
when(mock.methodThatReturnsAFuture())
.thenAnswer((_) async => 'Stub');
参数匹配器
Mockito 提供了参数匹配器(Argument Matchers),可以更灵活地匹配参数:
when(cat.eatFood(any)).thenReturn(false);
// ... or plain arguments themselves
when(cat.eatFood("fish")).thenReturn(true);
// ... including collections
when(cat.walk(["roof", "tree"])).thenReturn(2);
// ... or matchers
when(cat.eatFood(argThat(startsWith("dry")))).thenReturn(false);
命名参数
命名参数和参数匹配器需要明确指定参数名称:
when(cat.eatFood(any, hungry: anyNamed('hungry'))).thenReturn(true);
验证调用次数
你可以验证方法被调用了多少次:
verify(cat.sound()).called(2);
捕获参数
有时候我们需要捕获传入模拟方法的参数来进行进一步断言:
expect(verify(cat.eatFood(captureAny)).captured.single, "Fish");
调试
如果测试失败,可以打印所有收集到的调用记录:
logInvocations([catOne, catTwo]);
或者让每次未匹配到存根的调用都抛出异常:
throwOnMissingStub(cat);
示例代码
下面是一个完整的示例代码,展示了如何使用上述功能:
import 'dart:async';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
import 'example.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;
}
class FakeCat extends Fake implements Cat {
@override
bool? eatFood(String? food, {bool? hungry}) {
print('Fake eat $food');
return true;
}
}
abstract class Callbacks {
Cat findCat(String name);
String? makeSound();
}
@GenerateMocks([
Cat,
Callbacks,
], customMocks: [
MockSpec<Cat>(
as: #MockCatRelaxed,
onMissingStub: OnMissingStub.returnDefault,
),
])
void main() {
late Cat cat;
setUp(() {
cat = MockCat();
});
test("Let's verify some behaviour!", () {
when(cat.sound()).thenReturn('Meow');
cat.sound();
verify(cat.sound());
});
test('How about some stubbing?', () {
expect(() => cat.sound(), throwsA(isA<MissingStubError>()));
when(cat.sound()).thenReturn('Purr');
expect(cat.sound(), 'Purr');
when(cat.sound()).thenReturn('Meow');
expect(cat.sound(), 'Meow');
when(cat.lives).thenReturn(9);
expect(cat.lives, 9);
when(cat.lives).thenThrow(RangeError('Boo'));
expect(() => cat.lives, throwsRangeError);
final responses = ['Purr', 'Meow'];
when(cat.sound()).thenAnswer((_) => responses.removeAt(0));
expect(cat.sound(), 'Purr');
expect(cat.sound(), 'Meow');
});
test('Argument matchers', () {
when(cat.eatFood(any)).thenReturn(false);
when(cat.eatFood('fish')).thenReturn(true);
when(cat.walk(['roof', 'tree'])).thenReturn(2);
when(cat.eatFood(argThat(startsWith('dry')))).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'))));
cat.lives = 9;
verify(cat.lives = 9);
cat.hunt('backyard', null);
verify(cat.hunt('backyard', null));
cat.hunt('backyard', null);
verify(cat.hunt(argThat(contains('yard')),
argThat(isNull)));
});
test('Named arguments', () {
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);
});
test('Verifying exact number of invocations / at least x / never', () {
when(cat.sound()).thenReturn('Meow');
cat.sound();
cat.sound();
verify(cat.sound()).called(2);
cat.sound();
cat.sound();
cat.sound();
verify(cat.sound()).called(greaterThan(1));
verifyNever(cat.eatFood(any));
});
test('Verification in order', () {
when(cat.sound()).thenReturn('Meow');
when(cat.eatFood(any)).thenReturn(true);
cat.eatFood('Milk');
cat.sound();
cat.eatFood('Fish');
verifyInOrder([cat.eatFood('Milk'), cat.sound(), cat.eatFood('Fish')]);
});
test('Making sure interaction(s) never happened on mock', () {
verifyZeroInteractions(cat);
});
test('Finding redundant invocations', () {
when(cat.sound()).thenReturn('Meow');
cat.sound();
verify(cat.sound());
verifyNoMoreInteractions(cat);
});
test('Capturing arguments for further assertions', () {
when(cat.eatFood(any)).thenReturn(true);
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']);
});
test('Waiting for an interaction', () async {
when(cat.eatFood(any)).thenReturn(true);
Future<void> chewHelper(Cat cat) {
return cat.chew();
}
unawaited(chewHelper(cat));
await untilCalled(cat.chew());
cat.eatFood('Fish');
await untilCalled(cat.eatFood(any));
});
test('Mocked callbacks', () {
final makeSoundCallback = MockCallbacks().makeSound;
when(makeSoundCallback()).thenReturn('woof');
expect(makeSoundCallback(), 'woof');
final findCatCallback = MockCallbacks().findCat;
final mockCat = MockCat();
when(findCatCallback('Pete')).thenReturn(mockCat);
when(mockCat.sound()).thenReturn('meow');
expect(findCatCallback('Pete').sound(), 'meow');
});
test('Fake class', () {
final cat = FakeCat();
cat.eatFood('Milk');
expect(() => cat.sleep(), throwsUnimplementedError);
});
test('Relaxed mock class', () {
final cat = MockCatRelaxed();
cat.sleep();
expect(cat.lives, 0);
expect(cat.sound(), null);
expect(cat.eatFood('Milk'), null);
verify(cat.sleep());
});
}
以上就是关于如何在 Flutter 中使用 mockito 进行单元测试的基本介绍和示例。希望对你有所帮助!
更多关于Flutter单元测试模拟插件mockito的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
更多关于Flutter单元测试模拟插件mockito的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
在Flutter开发中,使用Mockito库可以方便地模拟插件(或其他依赖)的行为,从而进行单元测试。以下是一个使用Mockito进行Flutter单元测试的示例代码案例。
1. 添加依赖
首先,确保在你的pubspec.yaml
文件中添加了Mockito和test依赖:
dependencies:
flutter:
sdk: flutter
dev_dependencies:
test: ^1.16.0
mockito: ^4.0.0
2. 创建一个插件接口
假设我们有一个插件接口MyPlugin
,它有一个方法fetchData
:
// my_plugin.dart
import 'dart:async';
abstract class MyPlugin {
Future<String> fetchData();
}
3. 实现插件接口(模拟实现)
在实际应用中,你会有一个具体的插件实现。但在测试中,我们将使用Mockito来模拟它:
// my_plugin_impl.dart
import 'package:my_app/my_plugin.dart';
class MyPluginImpl implements MyPlugin {
@override
Future<String> fetchData() async {
// 实际的插件实现
return "Real Data";
}
}
4. 使用Mockito模拟插件
接下来,我们编写单元测试,并使用Mockito来模拟MyPlugin
的行为:
// my_plugin_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:my_app/my_plugin.dart';
void main() {
late MyPlugin mockPlugin;
setUp(() {
// 初始化Mockito模拟对象
mockPlugin = MockMyPlugin();
});
tearDown(() {
// 清理Mockito模拟对象
mockPlugin.close();
});
test('fetchData returns mocked data', () async {
// 设置模拟对象的行为
when(mockPlugin.fetchData()).thenAnswer((_) async => "Mocked Data");
// 调用被测试的方法
String result = await mockPlugin.fetchData();
// 验证结果
expect(result, "Mocked Data");
// 验证fetchData方法被调用了一次
verify(mockPlugin.fetchData()).called(1);
});
}
// 定义一个Mock类,继承自Mock并实现MyPlugin接口
class MockMyPlugin extends Mock implements MyPlugin {}
5. 运行测试
确保你的测试文件位于test/
目录下,然后运行测试:
flutter test
注意事项
-
Mock类的定义:在上面的示例中,我们定义了一个
MockMyPlugin
类,它继承自Mock
并实现了MyPlugin
接口。这是Mockito的标准用法。 -
设置和清理:在
setUp
方法中初始化模拟对象,在tearDown
方法中清理模拟对象。这是为了确保每个测试都是独立的,并且不会受到前一个测试的影响。 -
验证行为:使用
when
和thenAnswer
来设置模拟对象的行为,使用verify
来验证方法调用次数。
通过上述步骤,你可以使用Mockito在Flutter单元测试中模拟插件的行为,从而专注于测试你的业务逻辑。