Flutter单元测试:Mockito多存根引发MissingStubError问题
Mockito MissingStubError 解决与方法调用验证
问题场景
使用Mockito 5.3.2测试BookingService的createBooking方法时,触发MissingStubError,提示调用createCashbookItem(Instance of 'CashbookItem')无匹配存根。该方法逻辑为:当传入的Receipt的paymentType等于kPaymentTypeBar时,会调用CashbookItemRepository的createCashbookItem方法。
原因分析
Mockito默认使用严格对象实例相等性匹配存根。测试代码中手动创建了cashbookItem并基于该实例设置存根,但待测试方法createBooking内部是通过CashbookItem.fromReceipt(receipt)生成全新的CashbookItem实例——两个实例内存地址不同,因此Mockito无法匹配到对应的存根。
解决方案
方案1:使用参数匹配器(推荐)
通过Mockito的参数匹配器,忽略实例本身,匹配对象的核心属性。
步骤1:定义自定义匹配器
创建一个匹配器函数,对比CashbookItem的关键属性:
Matcher matchesCashbookItem(CashbookItem expected) { return predicate((actual) { final item = actual as CashbookItem; // 根据业务需求,对比需要验证的核心属性 return item.amount == expected.amount && item.paymentType == expected.paymentType && item.category == expected.category && item.documentNumber == expected.documentNumber; }); }
步骤2:更新存根逻辑
用自定义匹配器替换原有的实例匹配:
when(mockCashbookItemRepository.createCashbookItem(argThat(matchesCashbookItem(cashbookItem)))) .thenAnswer((_) async => const Right(unit));
如果不需要精确匹配,也可以用any()快速解决(适合简单场景):
when(mockCashbookItemRepository.createCashbookItem(any())) .thenAnswer((_) async => const Right(unit));
方案2:为CashbookItem实现相等性判断
重写CashbookItem类的==运算符和hashCode方法,让内容相同的实例被认为相等:
class CashbookItem { // 类属性定义 final double amount; final String paymentType; final String category; final String documentNumber; CashbookItem({ required this.amount, required this.paymentType, required this.category, required this.documentNumber, // 其他属性 }); // 从Receipt创建的方法 factory CashbookItem.fromReceipt(Receipt receipt) { return CashbookItem( amount: receipt.amount, paymentType: receipt.paymentType, category: receipt.category, documentNumber: receipt.documentNumber, // 其他属性映射 ); } @override bool operator ==(Object other) => identical(this, other) || other is CashbookItem && runtimeType == other.runtimeType && amount == other.amount && paymentType == other.paymentType && category == other.category && documentNumber == other.documentNumber; @override int get hashCode => Object.hash(amount, paymentType, category, documentNumber); }
完整测试代码(方案1示例)
@GenerateMocks([CashbookRepository, ReceiptRepository, CashbookItemRepository]) void main() { late BookingService bookingService; late MockCashbookRepository mockCashbookRepository; late MockCashbookItemRepository mockCashbookItemRepository; late MockReceiptRepository mockReceiptRepository; setUp(() { mockCashbookRepository = MockCashbookRepository(); mockCashbookItemRepository = MockCashbookItemRepository(); mockReceiptRepository = MockReceiptRepository(); bookingService = BookingServiceImpl( receiptRepository: mockReceiptRepository, cashbookItemRepository: mockCashbookItemRepository, cashbookRepository: mockCashbookRepository, ); }); // 自定义匹配器 Matcher matchesCashbookItem(CashbookItem expected) { return predicate((actual) { final item = actual as CashbookItem; return item.amount == expected.amount && item.paymentType == expected.paymentType && item.category == expected.category && item.documentNumber == expected.documentNumber; }); } group("bookings", () { var t_OldReceipt = Receipt.empty(); test("create booking should call createCashbookItem when paymentType is bar", () async { // Arrange t_OldReceipt = t_OldReceipt.copyWith( amount: 10, totalAmount: 10, taking: true, category: "test", taxRate: 0, paymentType: kPaymentTypeBar, documentNumber: "1", taxAmount: 0, receiptDate: DateTime.now(), year: "2023", yearMonth: "20232", yearMonthDay: "2023210", contact: Contact.empty()); final cashbookItem = CashbookItem.fromReceipt(t_OldReceipt); when(mockReceiptRepository.createReceipt(t_OldReceipt)) .thenAnswer((_) async => const Right(unit)); when(mockCashbookItemRepository.createCashbookItem(argThat(matchesCashbookItem(cashbookItem)))) .thenAnswer((_) async => const Right(unit)); // Act final result = await bookingService.createBooking(t_OldReceipt); // Assert expect(result, const Right(unit)); // 验证createCashbookItem是否被调用一次 verify(mockCashbookItemRepository.createCashbookItem(argThat(matchesCashbookItem(cashbookItem)))).called(1); // 验证receiptRepository的createReceipt被调用 verify(mockReceiptRepository.createReceipt(t_OldReceipt)).called(1); }); }); }
验证方法调用
使用Mockito的verify()方法确认createCashbookItem是否被正确调用:
- 精确验证:结合自定义匹配器,确保传入的参数符合预期
- 次数验证:通过
.called(n)指定调用次数(n为整数,比如1表示调用一次)
内容的提问来源于stack exchange,提问作者Matthias Mayer
相关产品推荐
相关产品推荐

