某些情况下,单元测试可能会依赖需要从线上 Web 服务或数据库中获取数据的类。这样会带来一些不便,原因如下:

  • 访问线上服务或数据库会拖慢测试执行效率。

  • 原本可以通过的测试可能会失败,因为 Web 服务或数据库可能会返回不符合预期的结果。这种情况被称作“flaky test”。

  • 使用线上 web 服务或数据库来测试很难覆盖全所有可能成功或失败的场景。

因此,最好不要依赖线上 web 服务或数据库,我们可以把这些依赖 “模拟(mock)”出来 。模拟(Mocks)允许我们仿造一个线上服务或数据库,并且可以根据条件返回特定结果。

通常来说,可以通过创建类的另一种实现来模拟(mock)这种依赖。类的另一种实现可以手写,也可以借助 Mockito 包,后者简单一些。

本篇教程只介绍了 Mockito 包的基本用法,更多用法请参考 Mockito 包文档

使用步骤

  • 添加 mockitotest 依赖

  • 创建一个要测试的函数

  • 创建一个模拟了 http.Client 的测试文件

  • 给每一个条件写一个测试

  • 执行这些测试

1. 添加 mockito 依赖

为了使用 mockito 包,首先将其和 flutter_test 的依赖一起添加到 pubspec.yaml 文件的 dev_dependencies 部分:

本例中还使用了 http 包,需要添加到 dependencies 部分:

  1. dependencies:
  2. http: <newest_version>
  3. dev_dependencies:
  4. test: <newest_version>
  5. mockito: <newest_version>

2. 创建一个要测试的函数

本例中,我们要对获取网络数据章节的 fetchPost 函数进行单元测试。为了便于测试,我们需要做两个改动:

  • 给函数提供一个 http.Client。这样的话我们可以在不同情形下提供相应的 http.Client 实例。如果是 Flutter 以及服务端项目,可以提供 http.IOClient 。如果是浏览器应用,可以提供 http.BrowserClient。为了测试,我们要提供一个模拟的 http.Client

  • 使用上面提供的 client 来请求网络数据,不要用 http.get 这个静态方法,因为它比较难以模拟。

函数经过改动之后:

  1. Future<Post> fetchPost(http.Client client) async {
  2. final response =
  3. await client.get('https://jsonplaceholder.typicode.com/posts/1');
  4. if (response.statusCode == 200) {
  5. // If the call to the server was successful, parse the JSON
  6. return Post.fromJson(json.decode(response.body));
  7. } else {
  8. // If that call was not successful, throw an error.
  9. throw Exception('Failed to load post');
  10. }
  11. }

3. 创建一个模拟了 http.Client 的测试文件

接下来,创建测试文件,我们需要在文件中创建 MockitoClient 类。遵循单元测试介绍章节的建议,我们在根目录下的 test 文件夹中创建一个名字为 fetch_post_test.dart 的文件。

MockClient 类会实现 http.Client 类。如此一来,我们就可以把 MockClient 传给 fetchPost 函数,还可以在每个测试中返回不同的 http 请求结果。

  1. // Create a MockClient using the Mock class provided by the Mockito package.
  2. // Create new instances of this class in each test.
  3. class MockClient extends Mock implements http.Client {}
  4. main() {
  5. // Tests go here
  6. }

4. 给每一个条件写一个测试

回过头来看,fetchPost 函数会完成下面两件事中的一件:

  • 如果 http 请求成功,返回 Post

  • 如果 http 请求失败,抛出 Exception

因此,我们要测试这两种条件。可以使用 MockClient 类为成功的测试返回一个 “OK” 的请求结果,为不成功的测试返回一个错误的请求结果。

我们使用 Mockito 的 when 函数来达到以上目的:

  1. // Create a MockClient using the Mock class provided by the Mockito package.
  2. // Create new instances of this class in each test.
  3. class MockClient extends Mock implements http.Client {}
  4. main() {
  5. group('fetchPost', () {
  6. test('returns a Post if the http call completes successfully', () async {
  7. final client = MockClient();
  8. // Use Mockito to return a successful response when it calls the
  9. // provided http.Client.
  10. when(client.get('https://jsonplaceholder.typicode.com/posts/1'))
  11. .thenAnswer((_) async => http.Response('{"title": "Test"}', 200));
  12. expect(await fetchPost(client), isInstanceOf<Post>());
  13. });
  14. test('throws an exception if the http call completes with an error', () {
  15. final client = MockClient();
  16. // Use Mockito to return an unsuccessful response when it calls the
  17. // provided http.Client.
  18. when(client.get('https://jsonplaceholder.typicode.com/posts/1'))
  19. .thenAnswer((_) async => http.Response('Not Found', 404));
  20. expect(fetchPost(client), throwsException);
  21. });
  22. });
  23. }

5. 执行测试

现在我们有了一个带测试的 fetchPost 函数,开始执行测试!

  1. $ dart test/fetch_post_test.dart

你也可以参考单元测试介绍章节用自己喜欢的编辑器来执行测试。

总结

通过本例,我们已经学会了如何用 Mockito 来测试对 web 服务或数据库有依赖的函数或类。这里只是简短地介绍了 Mockito 库以及模拟(mocking)的概念。更多内容请移步至 Mockito package