Flutter单元测试模拟插件mocktail的使用
Flutter单元测试模拟插件mocktail的使用
🍹 mocktail
Mocktail 是一个受 mockito 启发的 Dart 模拟库。它专注于提供一个熟悉的、简单的 API 来创建 Dart 中的模拟(带有空安全),而无需手动模拟或代码生成。
创建一个 Mock
import 'package:mocktail/mocktail.dart';
// 一个真实的 Cat 类
class Cat {
String sound() => 'meow!';
bool likes(String food, {bool isHungry = false}) => false;
final int lives = 9;
}
// 一个 Mock Cat 类
class MockCat extends Mock implements Cat {}
void main() {
// 创建一个 Mock Cat 实例
final cat = MockCat();
}
模拟和验证行为
// 模拟 `sound` 方法。
when(() => cat.sound()).thenReturn('meow');
// 验证没有交互发生。
verifyNever(() => cat.sound());
// 与 mock cat 实例交互。
cat.sound();
// 验证交互发生。
verify(() => cat.sound()).called(1);
// 当 mocktail 验证调用时,它将被排除在进一步的验证之外。
verifyNever(() => cat.sound());
// 再次与 mock 实例交互。
cat.sound();
// 验证计数为 1,因为自上次验证以来只有 1 次调用。
verify(() => cat.sound()).called(1);
其他用法
// 在与 mock 交互之前模拟方法。
when(() => cat.sound()).thenReturn('purrr!');
expect(cat.sound(), 'purrr!');
// 可以多次与 mock 交互。
expect(cat.sound(), 'purrr!');
// 可以更改模拟。
when(() => cat.sound()).thenReturn('meow!');
expect(cat.sound(), 'meow');
// 可以模拟 getter。
when(() => cat.lives).thenReturn(10);
expect(cat.lives, 10);
// 可以为特定参数模拟方法。
when(() => cat.likes('fish', isHungry: false)).thenReturn(true);
expect(cat.likes('fish', isHungry: false), isTrue);
// 可以验证特定参数的交互。
verify(() => cat.likes('fish', isHungry: false)).called(1);
// 或者使用 any(that: ...) 使用匹配器。
verify(() => cat.likes(any(that: isA<String>().having((food) => food, 'name', 'fish')))).called(1);
// 使用参数匹配器:`any`
// 对于位置参数,使用 `any()`。
// 对于命名参数,使用 `any(named: '<argName>')`。
// 可以使用 `any(that: customMatcher)` 提供自定义匹配器。
when(() => cat.likes(any(), isHungry: any(named: 'isHungry', that: isFalse))).thenReturn(true);
expect(cat.likes('fish', isHungry: false), isTrue);
// 可以模拟方法抛出异常。
when(() => cat.sound()).thenThrow(Exception('oops'));
expect(() => cat.sound(), throwsA(isA<Exception>()));
// 可以动态计算模拟。
final sounds = ['purrr', 'meow'];
when(() => cat.sound()).thenAnswer((_) => sounds.removeAt(0));
expect(cat.sound(), 'purrr');
expect(cat.sound(), 'meow');
// 可以捕获任何参数。
when(() => cat.likes('fish')).thenReturn(true);
expect(cat.likes('fish'), isTrue);
final captured = verify(() => cat.likes(captureAny())).captured;
expect(captured.last, equals(['fish']));
// 可以根据匹配器捕获特定参数。
when(() => cat.likes(any())).thenReturn(true);
expect(cat.likes('fish'), isTrue);
expect(cat.likes('dog food'), isTrue);
final captured = verify(() => cat.likes(captureAny(that: startsWith('d')))).captured;
expect(captured.last, equals(['dog food']));
重置 Mocks
reset(cat); // 重置模拟和交互
它是如何工作的
Mocktail 使用闭包来处理原本会传播并导致测试失败的 TypeError
实例。有关更多信息,请参阅 Issue #24。
为了支持参数匹配器(如 any
和 captureAny
),mocktail 必须为使用参数匹配器时注册默认返回值。开箱即用,它自动处理所有原始类型,但是当使用参数匹配器代替自定义类型时,开发者必须使用 registerFallbackValue
提供默认返回值。只需对每种类型调用一次 registerFallbackValue
,因此建议将所有 registerFallbackValue
调用放在 setUpAll
中。
class Food {}
class Cat {
bool likes(Food food) {}
}
...
class MockCat extends Mock implements Cat {}
class FakeFood extends Fake implements Food {}
void main() {
setUpAll(() {
registerFallbackValue(FakeFood());
});
test('...', () {
final cat = MockCat();
when(() => cat.likes(any())).thenReturn(true);
...
});
}
常见问题解答 (FAQs)
为什么我尝试伪造某些类(如 ThemeData 和 ColorScheme)时会遇到 invalid_implementation_override 错误?
这可能是由于 toString
方法签名的差异造成的,可以通过使用 mixin 解决:
mixin DiagnosticableToStringMixin on Object {
@override
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
return super.toString();
}
}
class FakeThemeData extends Fake
with DiagnosticableToStringMixin
implements ThemeData {}
为什么不能正确地模拟/验证扩展方法?
扩展方法不能被正确模拟/验证,因为它们被视为静态方法。这意味着调用直接转到扩展方法而不关心实例。因此,对扩展方法的模拟和验证总是会导致调用真实的方法。
为什么我看到错误:type ‘Null’ is not a subtype of type ‘Future<void>’?
默认情况下,当一个类继承 Mock
时,任何未模拟的方法返回 null
。例如:
class Person {
Future<void> sleep() {
await Future<void>.delayed(Duration(hours: 8));
}
}
class MockPerson extends Mock implements Person {}
final person = MockPerson();
when(() => person.sleep()).thenAnswer((_) async {});
为什么我的方法在使用 any()
进行模拟时会抛出 TypeError
?
默认情况下,当一个类继承 Mock
时,任何未模拟的方法返回 null
。当使用 any()
进行模拟时,类型必须可推断。然而,当一个方法具有泛型类型参数时,可能无法推断类型,因此泛型会回退到 dynamic
,导致方法表现得像未模拟一样。
class Cache {
bool set<T>(String key, T value) {
return true;
}
}
final cache = MockCache();
when(() => cache.set<int>(any(), any())).thenReturn((_) => true);
cache.set<int>('key', 1);
verify(() => cache.set<int>(any(), any())).called(1);
示例代码
import 'package:mocktail/mocktail.dart';
import 'package:test/test.dart';
class Food {}
class Chicken extends Food {}
class Tuna extends Food {}
// 一个真实的 Cat 类
class Cat {
String sound() => 'meow!';
bool likes(String food, {bool isHungry = false}) => false;
void eat<T extends Food>(T food) {}
final int lives = 9;
}
// 一个 Mock Cat 类
class MockCat extends Mock implements Cat {}
void main() {
group('Cat', () {
setUpAll(() {
// 注册 fallback 值,当使用
// `any` 或 `captureAny` 与自定义对象时。
registerFallbackValue(Chicken());
registerFallbackValue(Tuna());
});
late Cat cat;
setUp(() {
cat = MockCat();
});
test('example', () {
// 在与 mock 交互之前模拟方法。
when(() => cat.sound()).thenReturn('purr');
// 与 mock 交互。
expect(cat.sound(), 'purr');
// 验证交互。
verify(() => cat.sound()).called(1);
// 模拟带参数的方法
when(
() => cat.likes('fish', isHungry: any(named: 'isHungry')),
).thenReturn(true);
expect(cat.likes('fish', isHungry: true), isTrue);
// 验证交互。
verify(() => cat.likes('fish', isHungry: true)).called(1);
// 与 mock 交互。
cat
..eat(Chicken())
..eat(Tuna());
// 验证特定类型参数的交互。
verify(() => cat.eat<Chicken>(any())).called(1);
verify(() => cat.eat<Tuna>(any())).called(1);
verifyNever(() => cat.eat<Food>(any()));
});
});
}
更多关于Flutter单元测试模拟插件mocktail的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html
更多关于Flutter单元测试模拟插件mocktail的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
在Flutter开发中,单元测试是确保应用稳定性和质量的重要部分。当涉及到依赖第三方插件或外部服务时,直接调用这些依赖项可能会导致测试变得缓慢且不稳定。为了解决这个问题,我们可以使用mocktail库来模拟这些依赖项。
以下是一个使用mocktail进行Flutter单元测试的示例,假设我们有一个依赖于某个插件(例如some_plugin
)的服务。
1. 添加依赖
首先,在你的pubspec.yaml
文件中添加test
和mocktail
依赖:
dev_dependencies:
flutter_test:
sdk: flutter
mocktail: ^0.20.0 # 请检查最新版本号
2. 创建服务和接口
假设我们有一个SomePluginService
,它依赖于some_plugin
:
// some_plugin_service.dart
import 'package:some_plugin/some_plugin.dart';
class SomePluginService {
final SomePlugin somePlugin;
SomePluginService(this.somePlugin);
Future<String> fetchData() async {
return await somePlugin.fetchSomeData();
}
}
3. 编写单元测试
接下来,我们编写单元测试,并使用mocktail来模拟SomePlugin
:
// some_plugin_service_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:some_plugin/some_plugin.dart';
import 'some_plugin_service.dart';
class MockSomePlugin extends Mock implements SomePlugin {}
void main() {
late MockSomePlugin mockSomePlugin;
late SomePluginService somePluginService;
setUp(() {
mockSomePlugin = MockSomePlugin();
somePluginService = SomePluginService(mockSomePlugin);
});
test('fetchData returns expected data', async () {
// Arrange
const expectedData = 'mocked data';
when(mockSomePlugin.fetchSomeData()).thenAnswer((_) async => expectedData);
// Act
final result = await somePluginService.fetchData();
// Assert
expect(result, expectedData);
// Verify that the method was called exactly once
verify(mockSomePlugin.fetchSomeData()).called(1);
});
}
4. 运行测试
确保你的测试文件位于test/
目录下,然后运行以下命令来执行测试:
flutter test
解释
- 创建Mock类:我们使用
MockSomePlugin
类来模拟SomePlugin
。Mock
类是由mocktail库提供的。 - 设置测试环境:在
setUp
方法中,我们初始化mockSomePlugin
和somePluginService
。 - 编写测试用例:在
test
方法中,我们定义了测试的行为和期望结果。- 使用
when(...).thenAnswer(...)
来定义当mockSomePlugin.fetchSomeData()
被调用时应该返回什么。 - 调用
somePluginService.fetchData()
并断言返回结果是否与预期值匹配。 - 使用
verify
来验证fetchSomeData()
方法是否被调用了一次。
- 使用
通过这种方式,我们可以有效地隔离单元测试中的依赖项,使测试更加快速和稳定。