Flutter单元测试

511 阅读3分钟

1. 什么是单元测试

单元:程序的class、function 测试:测试某个特定条件下(或者场景)下某个特定函数的行为是否正常。

总:单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。

2.为什么要写单元测试

  • List item
  • 确保app在添加更多功能或更改现有功能时能继续稳定运行
  • 不需要编译、运行、打包才能测试,提升开发效率
  • 在验证的过程中,开发可以深度了解业务流程。别人看通过看单元测试就知道哪个逻辑跑了多少函数、需要注意哪些边界条件。

3. 编写单元测试的流程

  1. 引入测试框架

    dev_dependencies:
     test: <latest_version>
    

    test官方文档

  2. test目录下创建test file

    counter_app/
    	lib/
    	    counter.dart
    	test/
    	    counter_test.dart
    
  3. 编写测试代码 单一用例编写、用例组合

    例:简单计算函数

    class Counter {
     
      int count(int a, int b) {
        return a + b;
      };
    
      int sub(int a, int b) {
      	return a - b;
      };
    }
    
    import 'package:test/test.dart';
    import 'package:listener_test/counter.dart';
    
    void main() {
      late Counter counter;
      
      // Registers a function to be run before tests.
      setUp(() {
        counter = Counter();
      });
      
      //用例组合
      group('counter test', () {
      	//测试用例
        test('should count right', () {
          //验证实际和预期是否符合
          expect(counter.count(1, 2), 3);
        });
    
        test('should sub right', () {
          expect(counter.sub(3, 2), 1);
        });
      });
    }
    
  4. 运行测试代码

mock数据 有时,单元测试可能依赖从 服务器或数据库获取数据的类,这是不方便的,原因有几个:

  • 执行网络请求或查数据库会减慢测试执行速度。
  • 如果服务器或数据库返回意外结果,之前已经通过的测试用例可能会失败。这被称为"片状测试"。
  • 很难模拟所有可能的成功和失败场景。

因此,我们可以"模拟"这些数据源,而不是依赖真正的服务器或数据库,人为控制数据源的返回结果,从而验证所有的场景。用到的框架是mockito。 mockito文档

  1. 添加依赖

    dependencies:
      http: <newest_version>
    dev_dependencies:
      test: <latest_version>
      mockito: <newest_version>
      build_runner: <newest_version>
    
  2. 编写测试

    //测试函数
    Future<Album> fetchAlbum(http.Client client) async {
      final response = await client
          .get(Uri.parse('https://jsonplaceholder.typicode.com/albums/1'));
    
      if (response.statusCode == 200) {
        // If the server did return a 200 OK response,
        // then parse the JSON.
        return Album.fromJson(jsonDecode(response.body));
      } else {
        // If the server did not return a 200 OK response,
        // then throw an exception.
        throw Exception('Failed to load album');
      }
    }
    
  3. 生成需要mock的类的文件

    import 'package:http/http.dart' as http;
    import 'package:mockito/annotations.dart';
    
    // class MockClient extends Mock implements http.Client {}
    
    // Generate a MockClient using the Mockito package.
    // Create new instances of this class in each test.
    @GenerateMocks([http.Client])
    void main() {
    	
    }
    
  4. 编写测试代码 用例场景: 请求成功,返回album 请求失败,抛出异常

    import 'package:flutter_test/flutter_test.dart';
    import 'package:http/http.dart' as http;
    import 'package:mockito/annotations.dart';
    import 'package:mockito/mockito.dart';
    import 'package:mocking/main.dart';
    //上一步在同目录下生成了mocks文件,提供MockClient类
    import 'fetch_album_test.mocks.dart';
    
    @GenerateMocks([http.Client])
    void main() {
      group('fetchAlbum', () {
        test('returns an Album if the http call completes successfully', () async {
          final client = MockClient();
    
          //arrange
          when(client.get(Uri.parse('https://jsonplaceholder.typicode.com/albums/1')))
              .thenAnswer((_) async =>
                  http.Response('{"userId": 1, "id": 2, "title": "mock"}', 200));
    	  //act
    	  var response = await fetchAlbum(client);
    	  //verify
          expect(response, isA<Album>());
        });
    
        test('throws an exception if the http call completes with an error', () {
          final client = MockClient();
    
          //arrange
          when(client
                  .get(Uri.parse('https://jsonplaceholder.typicode.com/albums/1')))
              .thenAnswer((_) async => http.Response('Not Found', 404));
          //act and verify
          expect(fetchAlbum(client), throwsException);
        });
      });
    }
    

5.运行测试代码

业务中编写单元测试的场景

  • 工具类
  • repository、datesource
  • bloc

4.编写可测试的代码

几个原则:

  • 代码简洁清晰、可读性强
  • 可靠性强 只在所测的单元中真的有bug才会不通过,不能依赖任何单元外的东西,例如全局变量、环境、配置文件或方法的执行顺序等。当这些东西发生变化时,不会影响测试的结果。
  • 单一职责
  • 解耦