You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Dart中测试StreamSubscription并创建事件?如何为抽象Uploader类编写单元测试?

测试抽象Uploader类及StreamSubscription的Dart单元测试指南

嘿,我来帮你搞定这个Uploader类的单元测试问题,同时也会把Dart里测试StreamSubscription和模拟事件的方法讲清楚~

一、整体测试思路

因为Uploader是抽象类,咱们的核心测试目标是它的实现类FirebaseUploader的subscribe方法。测试的时候一定要mock掉Firebase的依赖(Auth、DatabaseReference),别真的去调用线上服务,不然测试既慢又不稳定。咱们要验证的是:方法有没有正确获取匿名用户ID、有没有用正确的参数创建DatabaseReference、有没有正确订阅流并触发回调。

二、准备测试依赖

首先得在pubspec.yaml里加好测试相关的包,推荐用mocktail来做mock,比老的mockito更简洁:

dev_dependencies:
  test: ^1.24.0
  mocktail: ^1.0.0

三、Mock Firebase依赖

咱们需要把FirebaseAuth、DatabaseReference这些依赖都mock出来,这样测试的时候就不会真的和Firebase交互了:

import 'package:mocktail/mocktail.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_database/firebase_database.dart';

// Mock FirebaseAuth
class MockFirebaseAuth extends Mock implements FirebaseAuth {}

// Mock UserCredential(匿名登录返回的对象)
class MockUserCredential extends Mock implements UserCredential {}

// Mock DatabaseReference
class MockDatabaseReference extends Mock implements DatabaseReference {}

// Mock StreamSubscription(用来验证订阅行为)
class MockStreamSubscription extends Mock implements StreamSubscription {}

四、编写FirebaseUploader的单元测试

接下来写具体的测试用例,一步步验证subscribe的逻辑:

import 'package:test/test.dart';
import 'package:your_app/path/to/uploader.dart';

void main() {
  late MockFirebaseAuth mockAuth;
  late MockDatabaseReference mockDbRef;
  late FirebaseUploader uploader;

  setUp(() {
    // 每次测试前初始化mock实例
    mockAuth = MockFirebaseAuth();
    mockDbRef = MockDatabaseReference();
    // 这里假设你的FirebaseUploader支持依赖注入,把mock的auth和db传进去
    uploader = FirebaseUploader(auth: mockAuth, database: mockDbRef);
  });

  test('subscribe方法应正确完成匿名登录并订阅数据库流', () async {
    // 1. 准备测试数据和mock行为
    const testFilename = 'vacation-photo.jpg';
    const testUserId = 'anonymous-12345';
    final mockUserCredential = MockUserCredential();
    
    // 模拟匿名登录返回指定的用户ID
    when(() => mockAuth.signInAnonymously()).thenAnswer((_) async => mockUserCredential);
    when(() => mockUserCredential.user?.uid).thenReturn(testUserId);

    // 模拟DatabaseReference返回一个包含测试事件的流
    final testEvent = Event('upload_progress', data: 70);
    final mockStream = Stream<Event>.fromIterable([testEvent]);
    when(() => mockDbRef.onValue).thenReturn(mockStream);
    
    // 模拟流订阅返回mock的StreamSubscription
    final mockSubscription = MockStreamSubscription();
    when(() => mockStream.listen(any())).thenReturn(mockSubscription);

    // 2. 执行测试方法,同时捕获回调的事件
    late Event capturedEvent;
    final subscription = await uploader.subscribe(testFilename, (event) {
      capturedEvent = event;
    });

    // 3. 验证所有预期行为是否发生
    // 验证是否调用了匿名登录
    verify(() => mockAuth.signInAnonymously()).called(1);
    // 验证是否用正确的userId和filename创建了DatabaseReference
    verify(() => uploader._databaseReference(testUserId, testFilename)).called(1);
    // 验证是否订阅了数据库的流
    verify(() => mockDbRef.onValue.listen(any())).called(1);
    // 验证返回的订阅对象是我们mock的实例
    expect(subscription, equals(mockSubscription));
    // 验证回调确实收到了测试事件
    expect(capturedEvent, equals(testEvent));
  });
}

五、测试StreamSubscription和模拟事件的核心技巧

上面的测试已经用到了这些技巧,这里单独拆解一下,方便你理解:

1. 创建测试用的Stream

有两种常用方式:

  • 快速创建包含固定事件的流:用Stream.fromIterable,适合简单场景
final testStream = Stream<Event>.fromIterable([
  Event('progress', data: 50),
  Event('complete', data: 'success'),
]);
  • 手动控制事件发送:用StreamController,适合需要动态触发事件的场景
final controller = StreamController<Event>();
// 发送一个进度事件
controller.add(Event('progress', data: 75));
// 发送一个完成事件
controller.add(Event('complete'));
// 关闭流
controller.close();

2. 验证StreamSubscription的行为

  • 验证listen是否被调用:用mocktail的verify方法,确认流的订阅逻辑被执行
  • 验证回调是否收到正确事件:像上面那样在测试中捕获回调的参数,然后断言是否符合预期
  • 验证取消订阅:如果你的逻辑里有取消订阅的操作,可以模拟cancel方法并验证调用:
when(() => mockSubscription.cancel()).thenAnswer((_) async {});
await subscription.cancel();
verify(() => mockSubscription.cancel()).called(1);

3. 测试错误场景

如果要测试流抛出错误的情况,可以创建错误流:

final errorStream = Stream<Event>.error(Exception('Upload failed'));
when(() => mockDbRef.onValue).thenReturn(errorStream);

// 测试错误回调(如果你的subscribe方法支持的话)
bool errorWasTriggered = false;
await uploader.subscribe(testFilename, (_) {}, onError: (error) {
  errorWasTriggered = true;
});
expect(errorWasTriggered, isTrue);

六、抽象类的契约测试(可选)

如果你想确保所有Uploader的实现都遵守抽象类的规则,可以写一个通用测试套件,这样以后加新的实现(比如本地文件上传的LocalUploader)可以直接复用:

// 通用契约测试
void testUploaderContract(Uploader Function() createUploader) {
  test('subscribe应返回非空的StreamSubscription', () async {
    final uploader = createUploader();
    final subscription = await uploader.subscribe('test.txt', (_) {});
    expect(subscription, isNotNull);
  });
}

// 在FirebaseUploader的测试中调用
void main() {
  testUploaderContract(() => FirebaseUploader(auth: MockFirebaseAuth(), database: MockDatabaseReference()));
  // 其他测试用例...
}

这样不管以后加多少个Uploader实现,都能快速验证它们符合抽象类的要求。


内容的提问来源于stack exchange,提问作者bizz84

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.25 04:12:45