From 40337326f825cb85221c8ce015de91e2d896abde Mon Sep 17 00:00:00 2001 From: Andrea Bizzotto Date: Mon, 14 Nov 2022 10:38:21 +0000 Subject: [PATCH] Updated unit tests for all AsyncNotifier subclasses # Conflicts: # ecommerce_app/test/src/features/authentication/presentation/account/account_screen_controller_test.dart --- .../account_screen_controller_test.dart | 113 +++++++++++---- ...mail_password_sign_in_controller_test.dart | 65 +++++---- .../add_to_cart_controller_test.dart | 115 +++++++++++----- .../shopping_cart_screen_controller_test.dart | 130 ++++++++++++------ .../payment_button_controller_test.dart | 71 +++++++--- .../leave_review_controller_test.dart | 100 +++++++++----- ecommerce_app/test/src/mocks.dart | 4 + 7 files changed, 410 insertions(+), 188 deletions(-) diff --git a/ecommerce_app/test/src/features/authentication/presentation/account/account_screen_controller_test.dart b/ecommerce_app/test/src/features/authentication/presentation/account/account_screen_controller_test.dart index 70de2dbe..7e8777f7 100644 --- a/ecommerce_app/test/src/features/authentication/presentation/account/account_screen_controller_test.dart +++ b/ecommerce_app/test/src/features/authentication/presentation/account/account_screen_controller_test.dart @@ -1,4 +1,5 @@ @Timeout(Duration(milliseconds: 500)) +import 'package:ecommerce_app/src/features/authentication/data/fake_auth_repository.dart'; import 'package:ecommerce_app/src/features/authentication/presentation/account/account_screen_controller.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -7,56 +8,112 @@ import 'package:mocktail/mocktail.dart'; import '../../../../mocks.dart'; void main() { - late MockAuthRepository authRepository; - late AccountScreenController controller; - setUp(() { - authRepository = MockAuthRepository(); - controller = AccountScreenController( - authRepository: authRepository, + ProviderContainer makeProviderContainer(MockAuthRepository authRepository) { + final container = ProviderContainer( + overrides: [ + authRepositoryProvider.overrideWithValue(authRepository), + ], ); + addTearDown(container.dispose); + return container; + } + + setUpAll(() { + registerFallbackValue(const AsyncLoading()); }); + group('AccountScreenController', () { - test('initial state is AsyncValue.data', () { + test('initial state is AsyncData', () { + final authRepository = MockAuthRepository(); + // create the ProviderContainer with the mock auth repository + final container = makeProviderContainer(authRepository); + // create a listener + final listener = Listener>(); + // listen to the provider and call [listener] whenever its value changes + container.listen( + accountScreenControllerProvider, + listener, + fireImmediately: true, + ); + // verify + verify( + // the build method returns a value immediately, so we expect AsyncData + () => listener(null, const AsyncData(null)), + ); + // verify that the listener is no longer called + verifyNoMoreInteractions(listener); + // verify that [signInAnonymously] was not called during initialization verifyNever(authRepository.signOut); - expect(controller.state, const AsyncData(null)); }); test('signOut success', () async { // setup - when(authRepository.signOut).thenAnswer( - (_) => Future.value(), - ); - // expect later - expectLater( - controller.stream, - emitsInOrder(const [ - AsyncLoading(), - AsyncData(null), - ]), + final authRepository = MockAuthRepository(); + // stub method to return success + when(authRepository.signOut).thenAnswer((_) => Future.value()); + // create the ProviderContainer with the mock auth repository + final container = makeProviderContainer(authRepository); + // create a listener + final listener = Listener>(); + // listen to the provider and call [listener] whenever its value changes + container.listen( + accountScreenControllerProvider, + listener, + fireImmediately: true, ); + // sto + const data = AsyncData(null); + // verify initial value from build method + verify(() => listener(null, data)); // run + final controller = + container.read(accountScreenControllerProvider.notifier); await controller.signOut(); // verify + verifyInOrder([ + // set loading state + // * use a matcher since AsyncLoading != AsyncLoading with data + // * https://codewithandrea.com/articles/unit-test-async-notifier-riverpod/ + () => listener(data, any(that: isA())), + // data when complete + () => listener(any(that: isA()), data), + ]); + verifyNoMoreInteractions(listener); verify(authRepository.signOut).called(1); }); test('signOut failure', () async { // setup + final authRepository = MockAuthRepository(); + // stub method to return success final exception = Exception('Connection failed'); when(authRepository.signOut).thenThrow(exception); - // expect later - expectLater( - controller.stream, - emitsInOrder([ - const AsyncLoading(), - predicate>((value) { - expect(value.hasError, true); - return true; - }), - ]), + // create the ProviderContainer with the mock auth repository + final container = makeProviderContainer(authRepository); + // create a listener + final listener = Listener>(); + // listen to the provider and call [listener] whenever its value changes + container.listen( + accountScreenControllerProvider, + listener, + fireImmediately: true, ); + const data = AsyncData(null); + // verify initial value from build method + verify(() => listener(null, data)); // run + final controller = + container.read(accountScreenControllerProvider.notifier); await controller.signOut(); // verify + verifyInOrder([ + // set loading state + // * use a matcher since AsyncLoading != AsyncLoading with data + () => listener(data, any(that: isA())), + // error when complete + () => listener( + any(that: isA()), any(that: isA())), + ]); + verifyNoMoreInteractions(listener); verify(authRepository.signOut).called(1); }); }); diff --git a/ecommerce_app/test/src/features/authentication/presentation/sign_in/email_password_sign_in_controller_test.dart b/ecommerce_app/test/src/features/authentication/presentation/sign_in/email_password_sign_in_controller_test.dart index 20553635..fa0b7a03 100644 --- a/ecommerce_app/test/src/features/authentication/presentation/sign_in/email_password_sign_in_controller_test.dart +++ b/ecommerce_app/test/src/features/authentication/presentation/sign_in/email_password_sign_in_controller_test.dart @@ -9,6 +9,10 @@ import 'package:mocktail/mocktail.dart'; import '../../../../mocks.dart'; void main() { + const testEmail = 'test@test.com'; + const testPassword = '1234'; + const testFormType = EmailPasswordSignInFormType.signIn; + ProviderContainer makeProviderContainer(MockAuthRepository authRepository) { final container = ProviderContainer( overrides: [ @@ -19,9 +23,9 @@ void main() { return container; } - const testEmail = 'test@test.com'; - const testPassword = '1234'; - const testFormType = EmailPasswordSignInFormType.signIn; + setUpAll(() { + registerFallbackValue(const AsyncLoading()); + }); group('EmailPasswordSignInController', () { test('sign in success', () async { @@ -32,17 +36,18 @@ void main() { testPassword, )).thenAnswer((_) => Future.value()); final container = makeProviderContainer(authRepository); - final controller = - container.read(emailPasswordSignInControllerProvider.notifier); - // expect later - expectLater( - controller.stream, - emitsInOrder([ - const AsyncLoading(), - const AsyncData(null), - ]), + final listener = Listener>(); + container.listen( + emailPasswordSignInControllerProvider, + listener, + fireImmediately: true, ); + const data = AsyncData(null); + // verify initial value from build method + verify(() => listener(null, data)); // run + final controller = + container.read(emailPasswordSignInControllerProvider.notifier); final result = await controller.submit( email: testEmail, password: testPassword, @@ -50,6 +55,13 @@ void main() { ); // verify expect(result, true); + verifyInOrder([ + // set loading state + () => listener(data, any(that: isA())), + // data when complete + () => listener(any(that: isA()), data), + ]); + verifyNoMoreInteractions(listener); }); test('sign in failure', () async { // setup @@ -60,20 +72,17 @@ void main() { testPassword, )).thenThrow(exception); final container = makeProviderContainer(authRepository); - final controller = - container.read(emailPasswordSignInControllerProvider.notifier); - // expect later - expectLater( - controller.stream, - emitsInOrder([ - const AsyncLoading(), - predicate>((state) { - expect(state.hasError, true); - return true; - }), - ]), + final listener = Listener>(); + container.listen( + emailPasswordSignInControllerProvider, + listener, + fireImmediately: true, ); + // verify initial value from build method + verify(() => listener(null, const AsyncData(null))); // run + final controller = + container.read(emailPasswordSignInControllerProvider.notifier); final result = await controller.submit( email: testEmail, password: testPassword, @@ -81,6 +90,14 @@ void main() { ); // verify expect(result, false); + verifyInOrder([ + // set loading state + () => listener( + const AsyncData(null), any(that: isA())), + // error when complete + () => listener( + any(that: isA()), any(that: isA())), + ]); }); }); } diff --git a/ecommerce_app/test/src/features/cart/presentation/add_to_cart/add_to_cart_controller_test.dart b/ecommerce_app/test/src/features/cart/presentation/add_to_cart/add_to_cart_controller_test.dart index a3d2a0fc..c0bf75b5 100644 --- a/ecommerce_app/test/src/features/cart/presentation/add_to_cart/add_to_cart_controller_test.dart +++ b/ecommerce_app/test/src/features/cart/presentation/add_to_cart/add_to_cart_controller_test.dart @@ -1,3 +1,4 @@ +import 'package:ecommerce_app/src/features/cart/application/cart_service.dart'; import 'package:ecommerce_app/src/features/cart/domain/item.dart'; import 'package:ecommerce_app/src/features/cart/presentation/add_to_cart/add_to_cart_controller.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -8,6 +9,21 @@ import '../../../../mocks.dart'; void main() { const productId = '1'; + + ProviderContainer makeProviderContainer(MockCartService cartService) { + final container = ProviderContainer( + overrides: [ + cartServiceProvider.overrideWithValue(cartService), + ], + ); + addTearDown(container.dispose); + return container; + } + + setUpAll(() { + registerFallbackValue(const AsyncLoading()); + }); + group('addItem', () { test('item added with quantity = 2, success', () async { // setup @@ -17,26 +33,42 @@ void main() { when(() => cartService.addItem(item)).thenAnswer( (_) => Future.value(null), ); - // run & verify - final controller = AddToCartController(cartService: cartService); - expect( - controller.debugState, - const AsyncData(1), + final container = makeProviderContainer(cartService); + final controller = container.read(addToCartControllerProvider.notifier); + final listener = Listener>(); + container.listen( + addToCartControllerProvider, + listener, + fireImmediately: true, ); + // run + const initialData = AsyncData(1); + // the build method returns a value immediately, so we expect AsyncData + verify(() => listener(null, initialData)); + // update quantity controller.updateQuantity(quantity); - expect( - controller.debugState, - const AsyncData(2), - ); - // * if desired, use expectLater and emitsInOrder here to check that - // * addItems emits two values + // the quantity is updated + verify(() => listener(initialData, const AsyncData(quantity))); + // add item await controller.addItem(productId); - verify(() => cartService.addItem(item)).called(1); - // check that quantity goes back to 1 after adding an item - expect( - controller.debugState, - const AsyncData(1), + verifyInOrder( + [ + // the loading state is set + () => listener( + const AsyncData(quantity), + const AsyncLoading() + .copyWithPrevious(const AsyncData(quantity)), + ), + // then the data is set with quantity: 1 + () => listener( + const AsyncLoading() + .copyWithPrevious(const AsyncData(quantity)), + initialData, + ), + ], ); + verifyNoMoreInteractions(listener); + verify(() => cartService.addItem(item)).called(1); }); test('item added with quantity = 2, failure', () async { @@ -45,30 +77,43 @@ void main() { final cartService = MockCartService(); when(() => cartService.addItem(item)) .thenThrow((_) => Exception('Connection failed')); - final controller = AddToCartController(cartService: cartService); - expect( - controller.debugState, - const AsyncData(1), + final container = makeProviderContainer(cartService); + final controller = container.read(addToCartControllerProvider.notifier); + final listener = Listener>(); + container.listen( + addToCartControllerProvider, + listener, + fireImmediately: true, ); + const initialData = AsyncData(1); + // the build method returns a value immediately, so we expect AsyncData + verify(() => listener(null, initialData)); + // update quantity controller.updateQuantity(quantity); - expect( - controller.debugState, - const AsyncData(2), + // the quantity is updated + verify( + () => listener(initialData, const AsyncData(quantity)), ); - // * if desired, use expectLater and emitsInOrder here to check that - // * addItems emits two values + // add item await controller.addItem(productId); - verify(() => cartService.addItem(item)).called(1); - // check that quantity goes back to 1 after adding an item - expect( - controller.debugState, - predicate>( - (value) { - expect(value.hasError, true); - return true; - }, - ), + verifyInOrder( + [ + // the loading state is set + () => listener( + const AsyncData(quantity), + const AsyncLoading() + .copyWithPrevious(const AsyncData(quantity)), + ), + // then an error is set + () => listener( + const AsyncLoading() + .copyWithPrevious(const AsyncData(quantity)), + any(that: isA()), + ), + ], ); + verifyNoMoreInteractions(listener); + verify(() => cartService.addItem(item)).called(1); }); }); } diff --git a/ecommerce_app/test/src/features/cart/presentation/shopping_cart/shopping_cart_screen_controller_test.dart b/ecommerce_app/test/src/features/cart/presentation/shopping_cart/shopping_cart_screen_controller_test.dart index 484bb9cd..d1d430fc 100644 --- a/ecommerce_app/test/src/features/cart/presentation/shopping_cart/shopping_cart_screen_controller_test.dart +++ b/ecommerce_app/test/src/features/cart/presentation/shopping_cart/shopping_cart_screen_controller_test.dart @@ -1,3 +1,4 @@ +import 'package:ecommerce_app/src/features/cart/application/cart_service.dart'; import 'package:ecommerce_app/src/features/cart/domain/item.dart'; import 'package:ecommerce_app/src/features/cart/presentation/shopping_cart/shopping_cart_screen_controller.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -8,6 +9,21 @@ import '../../../../mocks.dart'; void main() { const productId = '1'; + + ProviderContainer makeProviderContainer(MockCartService cartService) { + final container = ProviderContainer( + overrides: [ + cartServiceProvider.overrideWithValue(cartService), + ], + ); + addTearDown(container.dispose); + return container; + } + + setUpAll(() { + registerFallbackValue(const AsyncLoading()); + }); + group('updateItemQuantity', () { test('update quantity, success', () async { // setup @@ -16,16 +32,26 @@ void main() { when(() => cartService.setItem(item)).thenAnswer( (_) => Future.value(null), ); - final controller = ShoppingCartScreenController(cartService: cartService); - // run & verify - expectLater( - controller.stream, - emitsInOrder([ - const AsyncLoading(), - const AsyncData(null), - ]), + final container = makeProviderContainer(cartService); + final controller = + container.read(shoppingCartScreenControllerProvider.notifier); + final listener = Listener>(); + container.listen( + shoppingCartScreenControllerProvider, + listener, + fireImmediately: true, ); + const data = AsyncData(null); + verify(() => listener(null, data)); + // run await controller.updateItemQuantity(productId, 3); + // verify + verifyInOrder([ + () => listener(data, any(that: isA())), + () => listener(any(that: isA()), data), + ]); + verifyNoMoreInteractions(listener); + verify(() => cartService.setItem(item)).called(1); }); test('update quantity, failure', () async { @@ -35,22 +61,26 @@ void main() { when(() => cartService.setItem(item)).thenThrow( (_) => Exception('Connection failed'), ); - final controller = ShoppingCartScreenController(cartService: cartService); - // run & verify - expectLater( - controller.stream, - emitsInOrder([ - const AsyncLoading(), - predicate>( - (value) { - expect(value.hasError, true); - return true; - }, - ), - ]), + final container = makeProviderContainer(cartService); + final controller = + container.read(shoppingCartScreenControllerProvider.notifier); + final listener = Listener>(); + container.listen( + shoppingCartScreenControllerProvider, + listener, + fireImmediately: true, ); + const data = AsyncData(null); + verify(() => listener(null, data)); + // run await controller.updateItemQuantity(productId, 3); // verify + verifyInOrder([ + () => listener(data, any(that: isA())), + () => listener( + any(that: isA()), any(that: isA())), + ]); + verifyNoMoreInteractions(listener); verify(() => cartService.setItem(item)).called(1); }); }); @@ -61,16 +91,26 @@ void main() { when(() => cartService.removeItemById(productId)).thenAnswer( (_) => Future.value(null), ); - final controller = ShoppingCartScreenController(cartService: cartService); - // run & verify - expectLater( - controller.stream, - emitsInOrder([ - const AsyncLoading(), - const AsyncData(null), - ]), + final container = makeProviderContainer(cartService); + final controller = + container.read(shoppingCartScreenControllerProvider.notifier); + final listener = Listener>(); + container.listen( + shoppingCartScreenControllerProvider, + listener, + fireImmediately: true, ); + const data = AsyncData(null); + verify(() => listener(null, data)); + // run await controller.removeItemById(productId); + // verify + verifyInOrder([ + () => listener(data, any(that: isA())), + () => listener(any(that: isA()), data), + ]); + verifyNoMoreInteractions(listener); + verify(() => cartService.removeItemById(productId)).called(1); }); test('remove item, failure', () async { // setup @@ -78,21 +118,27 @@ void main() { when(() => cartService.removeItemById(productId)).thenThrow( (_) => Exception('Connection failed'), ); - final controller = ShoppingCartScreenController(cartService: cartService); - // run & verify - expectLater( - controller.stream, - emitsInOrder([ - const AsyncLoading(), - predicate>( - (value) { - expect(value.hasError, true); - return true; - }, - ), - ]), + final container = makeProviderContainer(cartService); + final controller = + container.read(shoppingCartScreenControllerProvider.notifier); + final listener = Listener>(); + container.listen( + shoppingCartScreenControllerProvider, + listener, + fireImmediately: true, ); + const data = AsyncData(null); + verify(() => listener(null, data)); + // run await controller.removeItemById(productId); + // verify + verifyInOrder([ + () => listener(data, any(that: isA())), + () => listener( + any(that: isA()), any(that: isA())), + ]); + verifyNoMoreInteractions(listener); + verify(() => cartService.removeItemById(productId)).called(1); }); }); } diff --git a/ecommerce_app/test/src/features/checkout/presentation/payment/payment_button_controller_test.dart b/ecommerce_app/test/src/features/checkout/presentation/payment/payment_button_controller_test.dart index a69ce49f..e9cf9000 100644 --- a/ecommerce_app/test/src/features/checkout/presentation/payment/payment_button_controller_test.dart +++ b/ecommerce_app/test/src/features/checkout/presentation/payment/payment_button_controller_test.dart @@ -1,3 +1,4 @@ +import 'package:ecommerce_app/src/features/checkout/application/fake_checkout_service.dart'; import 'package:ecommerce_app/src/features/checkout/presentation/payment/payment_button_controller.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -6,6 +7,20 @@ import 'package:mocktail/mocktail.dart'; import '../../../../mocks.dart'; void main() { + ProviderContainer makeProviderContainer(MockCheckoutService checkoutService) { + final container = ProviderContainer( + overrides: [ + checkoutServiceProvider.overrideWithValue(checkoutService), + ], + ); + addTearDown(container.dispose); + return container; + } + + setUpAll(() { + registerFallbackValue(const AsyncLoading()); + }); + group('pay', () { test('success', () async { // setup @@ -13,17 +28,26 @@ void main() { when(() => checkoutService.placeOrder()).thenAnswer( (_) => Future.value(null), ); + final container = makeProviderContainer(checkoutService); final controller = - PaymentButtonController(checkoutService: checkoutService); - // run & verify - expectLater( - controller.stream, - emitsInOrder([ - const AsyncLoading(), - const AsyncData(null), - ]), + container.read(paymentButtonControllerProvider.notifier); + final listener = Listener>(); + container.listen( + paymentButtonControllerProvider, + listener, + fireImmediately: true, ); + const data = AsyncData(null); + verify(() => listener(null, data)); + // run await controller.pay(); + // verify + verifyInOrder([ + () => listener(data, any(that: isA())), + () => listener(any(that: isA()), data), + ]); + verifyNoMoreInteractions(listener); + verify(() => checkoutService.placeOrder()).called(1); }); test('failure', () async { @@ -32,22 +56,27 @@ void main() { when(() => checkoutService.placeOrder()).thenThrow( Exception('Card declined'), ); + final container = makeProviderContainer(checkoutService); final controller = - PaymentButtonController(checkoutService: checkoutService); - // run & verify - expectLater( - controller.stream, - emitsInOrder([ - const AsyncLoading(), - predicate>( - (value) { - expect(value.hasError, true); - return true; - }, - ), - ]), + container.read(paymentButtonControllerProvider.notifier); + final listener = Listener>(); + container.listen( + paymentButtonControllerProvider, + listener, + fireImmediately: true, ); + const data = AsyncData(null); + verify(() => listener(null, data)); + // run await controller.pay(); + // verify + verifyInOrder([ + () => listener(data, any(that: isA())), + () => listener( + any(that: isA()), any(that: isA())), + ]); + verifyNoMoreInteractions(listener); + verify(() => checkoutService.placeOrder()).called(1); }); }); } diff --git a/ecommerce_app/test/src/features/reviews/presentation/leave_review_screen/leave_review_controller_test.dart b/ecommerce_app/test/src/features/reviews/presentation/leave_review_screen/leave_review_controller_test.dart index f19923c8..38a5a310 100644 --- a/ecommerce_app/test/src/features/reviews/presentation/leave_review_screen/leave_review_controller_test.dart +++ b/ecommerce_app/test/src/features/reviews/presentation/leave_review_screen/leave_review_controller_test.dart @@ -1,5 +1,7 @@ +import 'package:ecommerce_app/src/features/reviews/application/reviews_service.dart'; import 'package:ecommerce_app/src/features/reviews/domain/review.dart'; import 'package:ecommerce_app/src/features/reviews/presentation/leave_review_screen/leave_review_controller.dart'; +import 'package:ecommerce_app/src/utils/current_date_provider.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; @@ -17,31 +19,41 @@ void main() { ); const testProductId = '1'; - late MockReviewsService reviewsService; - setUp(() { - reviewsService = MockReviewsService(); + ProviderContainer makeProviderContainer(MockReviewsService reviewsService) { + final container = ProviderContainer( + overrides: [ + reviewsServiceProvider.overrideWithValue(reviewsService), + currentDateBuilderProvider.overrideWithValue(() => testDate), + ], + ); + addTearDown(container.dispose); + return container; + } + + setUpAll(() { + registerFallbackValue(const AsyncLoading()); }); group('submitReview', () { test('success', () async { // setup + final reviewsService = MockReviewsService(); when(() => reviewsService.submitReview( productId: testProductId, review: testReview, )).thenAnswer((_) => Future.value()); - final controller = LeaveReviewController( - reviewsService: reviewsService, - currentDateBuilder: () => testDate, + final container = makeProviderContainer(reviewsService); + final controller = container.read(leaveReviewControllerProvider.notifier); + final listener = Listener>(); + container.listen( + leaveReviewControllerProvider, + listener, + fireImmediately: true, ); - // run & verify + const data = AsyncData(null); + verify(() => listener(null, data)); + // run var didSucceed = false; - expectLater( - controller.stream, - emitsInOrder([ - const AsyncLoading(), - const AsyncData(null), - ]), - ); await controller.submitReview( previousReview: null, rating: testRating, @@ -49,6 +61,12 @@ void main() { productId: testProductId, onSuccess: () => didSucceed = true, ); + // verify + verifyInOrder([ + () => listener(data, any(that: isA())), + () => listener(any(that: isA()), data), + ]); + verifyNoMoreInteractions(listener); verify(() => reviewsService.submitReview( productId: testProductId, review: testReview, @@ -58,28 +76,23 @@ void main() { test('failure', () async { // setup + final reviewsService = MockReviewsService(); when(() => reviewsService.submitReview( productId: testProductId, review: testReview, )).thenThrow(Exception('Connection failed')); - final controller = LeaveReviewController( - reviewsService: reviewsService, - currentDateBuilder: () => testDate, + final container = makeProviderContainer(reviewsService); + final controller = container.read(leaveReviewControllerProvider.notifier); + final listener = Listener>(); + container.listen( + leaveReviewControllerProvider, + listener, + fireImmediately: true, ); - // run & verify + const data = AsyncData(null); + verify(() => listener(null, data)); + // run var didSucceed = false; - expectLater( - controller.stream, - emitsInOrder([ - const AsyncLoading(), - predicate>( - (value) { - expect(value.hasError, true); - return true; - }, - ) - ]), - ); await controller.submitReview( previousReview: null, rating: testRating, @@ -87,6 +100,13 @@ void main() { productId: testProductId, onSuccess: () => didSucceed = true, ); + // verify + verifyInOrder([ + () => listener(data, any(that: isA())), + () => listener( + any(that: isA()), any(that: isA())), + ]); + verifyNoMoreInteractions(listener); verify(() => reviewsService.submitReview( productId: testProductId, review: testReview, @@ -96,20 +116,23 @@ void main() { test('same data as before', () async { // setup + final reviewsService = MockReviewsService(); when(() => reviewsService.submitReview( productId: testProductId, review: testReview, )).thenThrow(Exception('Connection failed')); - final controller = LeaveReviewController( - reviewsService: reviewsService, - currentDateBuilder: () => testDate, + final container = makeProviderContainer(reviewsService); + final controller = container.read(leaveReviewControllerProvider.notifier); + final listener = Listener>(); + container.listen( + leaveReviewControllerProvider, + listener, + fireImmediately: true, ); - // run & verify + const data = AsyncData(null); + verify(() => listener(null, data)); + // run var didSucceed = false; - expectLater( - controller.stream, - emitsInOrder([]), // no state changes - ); await controller.submitReview( previousReview: testReview, rating: testRating, @@ -117,6 +140,7 @@ void main() { productId: testProductId, onSuccess: () => didSucceed = true, ); + verifyNoMoreInteractions(listener); verifyNever(() => reviewsService.submitReview( productId: testProductId, review: testReview, diff --git a/ecommerce_app/test/src/mocks.dart b/ecommerce_app/test/src/mocks.dart index 4024e8f5..d7d6317e 100644 --- a/ecommerce_app/test/src/mocks.dart +++ b/ecommerce_app/test/src/mocks.dart @@ -26,3 +26,7 @@ class MockCheckoutService extends Mock implements FakeCheckoutService {} class MockReviewsRepository extends Mock implements FakeReviewsRepository {} class MockReviewsService extends Mock implements ReviewsService {} + +class Listener extends Mock { + void call(T? previous, T next); +}