Flutter单元测试模拟插件mockito的使用

发布于 1周前 作者 nodeper 来自 Flutter

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

1 回复

更多关于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

注意事项

  1. Mock类的定义:在上面的示例中,我们定义了一个MockMyPlugin类,它继承自Mock并实现了MyPlugin接口。这是Mockito的标准用法。

  2. 设置和清理:在setUp方法中初始化模拟对象,在tearDown方法中清理模拟对象。这是为了确保每个测试都是独立的,并且不会受到前一个测试的影响。

  3. 验证行为:使用whenthenAnswer来设置模拟对象的行为,使用verify来验证方法调用次数。

通过上述步骤,你可以使用Mockito在Flutter单元测试中模拟插件的行为,从而专注于测试你的业务逻辑。

回到顶部