1. 什么是单元测试
单元:程序的class、function 测试:测试某个特定条件下(或者场景)下某个特定函数的行为是否正常。
总:单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。
2.为什么要写单元测试
- List item
- 确保app在添加更多功能或更改现有功能时能继续稳定运行
- 不需要编译、运行、打包才能测试,提升开发效率
- 在验证的过程中,开发可以深度了解业务流程。别人看通过看单元测试就知道哪个逻辑跑了多少函数、需要注意哪些边界条件。
3. 编写单元测试的流程
-
引入测试框架
dev_dependencies: test: <latest_version> -
test目录下创建test file
counter_app/ lib/ counter.dart test/ counter_test.dart -
编写测试代码 单一用例编写、用例组合
例:简单计算函数
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); }); }); } -
运行测试代码
mock数据 有时,单元测试可能依赖从 服务器或数据库获取数据的类,这是不方便的,原因有几个:
- 执行网络请求或查数据库会减慢测试执行速度。
- 如果服务器或数据库返回意外结果,之前已经通过的测试用例可能会失败。这被称为"片状测试"。
- 很难模拟所有可能的成功和失败场景。
因此,我们可以"模拟"这些数据源,而不是依赖真正的服务器或数据库,人为控制数据源的返回结果,从而验证所有的场景。用到的框架是mockito。 mockito文档
-
添加依赖
dependencies: http: <newest_version> dev_dependencies: test: <latest_version> mockito: <newest_version> build_runner: <newest_version> -
编写测试
//测试函数 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'); } } -
生成需要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() { } -
编写测试代码 用例场景: 请求成功,返回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才会不通过,不能依赖任何单元外的东西,例如全局变量、环境、配置文件或方法的执行顺序等。当这些东西发生变化时,不会影响测试的结果。
- 单一职责
- 解耦