dart `StateNotifierProvider`测试未捕获初始状态

5vf7fwbs  于 5个月前  发布在  其他
关注(0)|答案(1)|浏览(40)

我正在为我的StateNotifierProvider编写单元测试,但在测试失败场景时,我遇到了一个奇怪的场景。由于某种原因,单元测试没有捕获初始状态。下面是提供程序实现:

final paymentBundleVM = StateNotifierProvider.autoDispose<PaymentBundleVM, PaymentBundleState>(
  (ref) => PaymentBundleVM(ref.read(getPaymentBundleUseCase), ref.read(createPaymentBundleUseCase)),
  dependencies: [getPaymentBundleUseCase, createPaymentBundleUseCase],
  name: 'paymentBundleVM',
);

class PaymentBundleVM extends StateNotifier<PaymentBundleState> {
  PaymentBundleVM(this._getBundleUseCase, this._createBundleUseCase) : super(PaymentBundleLoading()) {
    unawaited(_loadDependencies());
  }

  final GetPaymentBundleUseCase _getBundleUseCase;
  final CreatePaymentBundleUseCase _createBundleUseCase;

  Future<void> _loadDependencies() async {
    try {
      final bundle = await _getBundleUseCase.run();
      if (mounted) {
        if (bundle != null)
          state = PaymentBundleLoaded(bundle);
        else
          state = PaymentBundleDisabled();
      }
    } on BaseException catch (exception) {
      if (mounted) state = PaymentBundleFailedLoadingState(exception);
    }
  }
}

abstract class PaymentBundleState extends Equatable {
  @override
  List<Object?> get props => [];
}

class PaymentBundleLoading extends PaymentBundleState {}

class PaymentBundleDisabled extends PaymentBundleState {}

class PaymentBundleLoaded extends PaymentBundleState {
  PaymentBundleLoaded(this.bundle);

  final PaymentBundle bundle;

  @override
  List<Object?> get props => [...super.props, bundle];
}

class PaymentBundleFailedLoadingState extends PaymentBundleState {
  PaymentBundleFailedLoadingState(this.exception);

  final BaseException exception;

  @override
  List<Object?> get props => [...super.props, exception];
}

字符串
下面是我的单元测试代码:

void main() {
  final getPaymentBundleUseCaseMock = GetPaymentBundleUseCaseMock();
  final createPaymentBundleUseCaseMock = CreatePaymentBundleUseCaseMock();

  late ProviderContainer container;

  final fakeBundle = PaymentBundleMock();

  setUp(() {
    container = ProviderContainer(
      overrides: [
        getPaymentBundleUseCase.overrideWith((ref) => getPaymentBundleUseCaseMock),
        createPaymentBundleUseCase.overrideWith((ref) => createPaymentBundleUseCaseMock),
      ],
    );
  });

  tearDown(() {
    reset(getPaymentBundleUseCaseMock);
    reset(createPaymentBundleUseCaseMock);
  });

  test('test A', () async {
    when(getPaymentBundleUseCaseMock.run).thenAnswer((_) => Future.value(fakeBundle));
    final listener = ListenerMock<PaymentBundleState>();
    container.listen<PaymentBundleState>(paymentBundleVM, listener, fireImmediately: true);

    await container.pump();

    verifyInOrder(
      [
        () => listener(null, PaymentBundleLoading()),
        () => listener(PaymentBundleLoading(), PaymentBundleLoaded(fakeBundle)),
      ],
    );

    verifyNoMoreInteractions(listener);
  });

  test('test B', () async {
    when(getPaymentBundleUseCaseMock.run).thenAnswer((_) => Future.value());
    final listener = ListenerMock<PaymentBundleState>();
    container.listen<PaymentBundleState>(paymentBundleVM, listener, fireImmediately: true);

    await container.pump();

    verifyInOrder(
      [
        () => listener(null, PaymentBundleLoading()),
        () => listener(PaymentBundleLoading(), PaymentBundleDisabled()),
      ],
    );

    verifyNoMoreInteractions(listener);
  });

  test('test C', () async {
    final exception = HttpException.handshake();
    when(getPaymentBundleUseCaseMock.run).thenThrow(exception);
    final listener = ListenerMock<PaymentBundleState>();
    container.listen<PaymentBundleState>(paymentBundleVM, listener, fireImmediately: true);

    await container.pump();

    verifyInOrder(
      [
        () => listener(null, PaymentBundleLoading()),
        () => listener(PaymentBundleLoading(), PaymentBundleFailedLoadingState(exception)),
      ],
    );

    verifyNoMoreInteractions(listener);
  });
}


让我感到奇怪的是,test Atest B都能按预期工作,但第三个却抛出了问题:

Matching call #0 not found. All calls: ListenerMock<PaymentBundleState>.call(
    null,
    PaymentBundleFailedLoadingState(        [BaseException - handshakeHttp] null.
      Server message: null
      User message: null
      
          statusCode: null
        ))


因此,基本上这个测试考虑的是StateNotifierProvider的第一个状态是null,之后它直接转换到PaymentBundleFailedLoadingState。问题是,为什么PaymentBundleLoading不在状态栈中考虑,因为它是提供程序示例化时发出的第一个状态?根据我的理解,预期的状态堆栈应该是null -> loading -> failedLoading,而不是null -> failedLoading

1zmg4dgp

1zmg4dgp1#

根据@Randal Schwartz的建议,我重构了代码,用AsyncNotifierProvider替换了StateNotifierProvider,这减少了大量的代码样板,提高了可测试性。我最终得到了以下提供程序和测试套件:
供应商:

final paymentBundleVM = AsyncNotifierProvider.autoDispose<PaymentBundleVM, PaymentBundle?>(
  PaymentBundleVM.new,
  dependencies: [getPaymentBundleUseCase, createPaymentBundleUseCase],
  name: 'paymentBundleVM',
);

class PaymentBundleVM extends AutoDisposeAsyncNotifier<PaymentBundle?> {
  @override
  FutureOr<PaymentBundle?> build() async {
    final bundle = await ref.read(getPaymentBundleUseCase).run();
    return bundle;
  }

  Future<void> createBundle() async {
    state = const AsyncValue.loading();
    state = await AsyncValue.guard(ref.read(createPaymentBundleUseCase).run);
  }
}

字符串
测试:

void main() {
  final getPaymentBundleUseCaseMock = GetPaymentBundleUseCaseMock();
  final createPaymentBundleUseCaseMock = CreatePaymentBundleUseCaseMock();

  late ProviderContainer container;

  final fakeBundle = PaymentBundleMock();

  setUp(() {
    registerFallbackValue(const AsyncValue<PaymentBundle?>.error(Object(), StackTrace.empty));
    registerFallbackValue(const AsyncValue<PaymentBundle?>.loading());

    container = ProviderContainer(
      overrides: [
        getPaymentBundleUseCase.overrideWith((ref) => getPaymentBundleUseCaseMock),
        createPaymentBundleUseCase.overrideWith((ref) => createPaymentBundleUseCaseMock),
      ],
    );
  });

  tearDown(() {
    reset(getPaymentBundleUseCaseMock);
    reset(createPaymentBundleUseCaseMock);
  });

  test('test A', () async {
    when(getPaymentBundleUseCaseMock.run).thenAnswer((_) => Future.value(fakeBundle));
    final listener = ListenerMock<AsyncValue<PaymentBundle?>>();
    container.listen<AsyncValue<PaymentBundle?>>(paymentBundleVM, listener, fireImmediately: true);

    await container.read(paymentBundleVM.future);

    const loading = AsyncValue<PaymentBundle?>.loading();

    verifyInOrder(
      [
        () => listener(null, loading),
        () => listener(loading, AsyncValue<PaymentBundle?>.data(fakeBundle)),
      ],
    );

    verifyNoMoreInteractions(listener);
  });

  test('test B', () async {
    when(getPaymentBundleUseCaseMock.run).thenAnswer((_) => Future.value());
    final listener = ListenerMock<AsyncValue<PaymentBundle?>>();
    container.listen<AsyncValue<PaymentBundle?>>(paymentBundleVM, listener, fireImmediately: true);

    await container.read(paymentBundleVM.future);

    const loading = AsyncValue<PaymentBundle?>.loading();

    verifyInOrder(
      [
        () => listener(null, loading),
        () => listener(loading, const AsyncValue<PaymentBundle?>.data(null)),
      ],
    );

    verifyNoMoreInteractions(listener);
  });

  test('test C', () async {
    final exception = HttpException.handshake();
    when(getPaymentBundleUseCaseMock.run).thenThrow(exception);
    final listener = ListenerMock<AsyncValue<PaymentBundle?>>();
    container.listen<AsyncValue<PaymentBundle?>>(paymentBundleVM, listener, fireImmediately: true);

    try {
      await container.read(paymentBundleVM.future);
    } catch (_) {}

    const loading = AsyncValue<PaymentBundle?>.loading();

    verifyInOrder(
      [
        () => listener(null, loading),
        () => listener(loading, any<AsyncError<PaymentBundle?>>()),
      ],
    );

    verifyNoMoreInteractions(listener);
  });
}

相关问题