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

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

🍹 mocktail

Pub mocktail coverage License: MIT

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

为了支持参数匹配器(如 anycaptureAny),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

1 回复

更多关于Flutter单元测试模拟插件mocktail的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


在Flutter开发中,单元测试是确保应用稳定性和质量的重要部分。当涉及到依赖第三方插件或外部服务时,直接调用这些依赖项可能会导致测试变得缓慢且不稳定。为了解决这个问题,我们可以使用mocktail库来模拟这些依赖项。

以下是一个使用mocktail进行Flutter单元测试的示例,假设我们有一个依赖于某个插件(例如some_plugin)的服务。

1. 添加依赖

首先,在你的pubspec.yaml文件中添加testmocktail依赖:

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

解释

  1. 创建Mock类:我们使用MockSomePlugin类来模拟SomePluginMock类是由mocktail库提供的。
  2. 设置测试环境:在setUp方法中,我们初始化mockSomePluginsomePluginService
  3. 编写测试用例:在test方法中,我们定义了测试的行为和期望结果。
    • 使用when(...).thenAnswer(...)来定义当mockSomePlugin.fetchSomeData()被调用时应该返回什么。
    • 调用somePluginService.fetchData()并断言返回结果是否与预期值匹配。
    • 使用verify来验证fetchSomeData()方法是否被调用了一次。

通过这种方式,我们可以有效地隔离单元测试中的依赖项,使测试更加快速和稳定。

回到顶部