From e39adb623c438f18eec814f9dda1797141d1d9c2 Mon Sep 17 00:00:00 2001 From: Andrea Bizzotto Date: Wed, 13 Jul 2022 08:46:26 +0100 Subject: [PATCH] Update unit, widget, integration tests --- .../integration_test/purchase_flow_test.dart | 16 ++-- .../application/fake_checkout_service.dart | 44 +++++---- .../features/authentication/auth_robot.dart | 7 ++ .../fake_checkout_service_test.dart | 92 +++++++++++++++++++ .../src/features/checkout/checkout_robot.dart | 26 ++++++ .../checkout_screen/checkout_screen_test.dart | 35 +++++++ .../payment_button_controller_test.dart | 53 +++++++++++ .../src/features/orders/orders_robot.dart | 17 ++++ .../test/src/features/purchase_flow_test.dart | 16 ++-- ecommerce_app/test/src/mocks.dart | 6 ++ ecommerce_app/test/src/robot.dart | 6 ++ 11 files changed, 284 insertions(+), 34 deletions(-) create mode 100644 ecommerce_app/test/src/features/checkout/application/fake_checkout_service_test.dart create mode 100644 ecommerce_app/test/src/features/checkout/checkout_robot.dart create mode 100644 ecommerce_app/test/src/features/checkout/presentation/checkout_screen/checkout_screen_test.dart create mode 100644 ecommerce_app/test/src/features/checkout/presentation/payment/payment_button_controller_test.dart create mode 100644 ecommerce_app/test/src/features/orders/orders_robot.dart diff --git a/ecommerce_app/integration_test/purchase_flow_test.dart b/ecommerce_app/integration_test/purchase_flow_test.dart index 852796b5..e3445857 100644 --- a/ecommerce_app/integration_test/purchase_flow_test.dart +++ b/ecommerce_app/integration_test/purchase_flow_test.dart @@ -15,15 +15,17 @@ void main() { await r.cart.addToCart(); await r.cart.openCart(); r.cart.expectFindNCartItems(1); - await r.closePage(); - // sign in - await r.openPopupMenu(); - await r.auth.openEmailPasswordSignInScreen(); + // checkout + await r.checkout.startCheckout(); await r.auth.signInWithEmailAndPassword(); - r.products.expectFindAllProductCards(); - // check cart again (to verify cart synchronization) - await r.cart.openCart(); r.cart.expectFindNCartItems(1); + await r.checkout.startPayment(); + // when a payment is complete, user is taken to the orders page + r.orders.expectFindNOrders(1); + await r.closePage(); // close orders page + // check that cart is now empty + await r.cart.openCart(); + r.cart.expectFindZeroCartItems(); await r.closePage(); // sign out await r.openPopupMenu(); diff --git a/ecommerce_app/lib/src/features/checkout/application/fake_checkout_service.dart b/ecommerce_app/lib/src/features/checkout/application/fake_checkout_service.dart index c092e4ae..47dc2645 100644 --- a/ecommerce_app/lib/src/features/checkout/application/fake_checkout_service.dart +++ b/ecommerce_app/lib/src/features/checkout/application/fake_checkout_service.dart @@ -26,26 +26,30 @@ class FakeCheckoutService { final uid = authRepository.currentUser!.uid; // 1. Get the cart object final cart = await remoteCartRepository.fetchCart(uid); - final total = _totalPrice(cart); - // * If we want to make this code more testable, a DateTime builder - // * should be injected as a dependency - final orderDate = DateTime.now(); - // * The orderId is a unique string that could be generated with the UUID - // * package. Since this is a fake service, we just derive it from the date. - final orderId = orderDate.toIso8601String(); - // 2. Create an order - final order = Order( - id: orderId, - userId: uid, - items: cart.items, - orderStatus: OrderStatus.confirmed, - orderDate: orderDate, - total: total, - ); - // 3. Save it using the repository - await ordersRepository.addOrder(uid, order); - // 4. Empty the cart - await remoteCartRepository.setCart(uid, const Cart()); + if (cart.items.isNotEmpty) { + final total = _totalPrice(cart); + // * If we want to make this code more testable, a DateTime builder + // * should be injected as a dependency + final orderDate = DateTime.now(); + // * The orderId is a unique string that could be generated with the UUID + // * package. Since this is a fake service, we just derive it from the date. + final orderId = orderDate.toIso8601String(); + // 2. Create an order + final order = Order( + id: orderId, + userId: uid, + items: cart.items, + orderStatus: OrderStatus.confirmed, + orderDate: orderDate, + total: total, + ); + // 3. Save it using the repository + await ordersRepository.addOrder(uid, order); + // 4. Empty the cart + await remoteCartRepository.setCart(uid, const Cart()); + } else { + throw StateError('Can\'t place an order if the cart is empty'); + } } // Helper method to calculate the total price diff --git a/ecommerce_app/test/src/features/authentication/auth_robot.dart b/ecommerce_app/test/src/features/authentication/auth_robot.dart index beee8b6d..89e0704b 100644 --- a/ecommerce_app/test/src/features/authentication/auth_robot.dart +++ b/ecommerce_app/test/src/features/authentication/auth_robot.dart @@ -69,6 +69,13 @@ class AuthRobot { await tester.enterText(passwordField, password); } + void expectEmailAndPasswordFieldsFound() { + final emailField = find.byKey(EmailPasswordSignInScreen.emailKey); + expect(emailField, findsOneWidget); + final passwordField = find.byKey(EmailPasswordSignInScreen.passwordKey); + expect(passwordField, findsOneWidget); + } + void expectCreateAccountButtonFound() { final dialogTitle = find.text('Create an account'); expect(dialogTitle, findsOneWidget); diff --git a/ecommerce_app/test/src/features/checkout/application/fake_checkout_service_test.dart b/ecommerce_app/test/src/features/checkout/application/fake_checkout_service_test.dart new file mode 100644 index 00000000..9947ce67 --- /dev/null +++ b/ecommerce_app/test/src/features/checkout/application/fake_checkout_service_test.dart @@ -0,0 +1,92 @@ +import 'package:ecommerce_app/src/features/authentication/data/fake_auth_repository.dart'; +import 'package:ecommerce_app/src/features/authentication/domain/app_user.dart'; +import 'package:ecommerce_app/src/features/cart/data/remote/remote_cart_repository.dart'; +import 'package:ecommerce_app/src/features/cart/domain/cart.dart'; +import 'package:ecommerce_app/src/features/checkout/application/fake_checkout_service.dart'; +import 'package:ecommerce_app/src/features/orders/data/fake_orders_repository.dart'; +import 'package:ecommerce_app/src/features/orders/domain/order.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../../mocks.dart'; + +void main() { + const testUser = AppUser(uid: 'abc'); + setUpAll(() { + // needed for MockOrdersRepository + registerFallbackValue(Order( + id: '1', + userId: testUser.uid, + items: {'1': 1}, + orderStatus: OrderStatus.confirmed, + orderDate: DateTime(2022, 7, 13), + total: 15, + )); + // needed for MockRemoteCartRepository + registerFallbackValue(const Cart()); + }); + + late MockAuthRepository authRepository; + late MockRemoteCartRepository remoteCartRepository; + late MockOrdersRepository ordersRepository; + setUp(() { + authRepository = MockAuthRepository(); + remoteCartRepository = MockRemoteCartRepository(); + ordersRepository = MockOrdersRepository(); + }); + + FakeCheckoutService makeCheckoutService() { + final container = ProviderContainer( + overrides: [ + authRepositoryProvider.overrideWithValue(authRepository), + remoteCartRepositoryProvider.overrideWithValue(remoteCartRepository), + ordersRepositoryProvider.overrideWithValue(ordersRepository), + ], + ); + addTearDown(container.dispose); + return container.read(checkoutServiceProvider); + } + + group('placeOrder', () { + test('null user, throws', () async { + // setup + when(() => authRepository.currentUser).thenReturn(null); + final checkoutService = makeCheckoutService(); + // run + expect(checkoutService.placeOrder, throwsA(isA())); + }); + + test('empty cart, throws', () async { + // setup + when(() => authRepository.currentUser).thenReturn(testUser); + when(() => remoteCartRepository.fetchCart(testUser.uid)).thenAnswer( + (_) => Future.value(const Cart()), + ); + final checkoutService = makeCheckoutService(); + // run + expect(checkoutService.placeOrder, throwsStateError); + }); + + test('non-empty cart, creates order', () async { + // setup + when(() => authRepository.currentUser).thenReturn(testUser); + when(() => remoteCartRepository.fetchCart(testUser.uid)).thenAnswer( + (_) => Future.value(const Cart({'1': 1})), + ); + when(() => ordersRepository.addOrder(testUser.uid, any())).thenAnswer( + (_) => Future.value(), + ); + when(() => remoteCartRepository.setCart(testUser.uid, const Cart())) + .thenAnswer( + (_) => Future.value(), + ); + final checkoutService = makeCheckoutService(); + // run + await checkoutService.placeOrder(); + // verify + verify(() => ordersRepository.addOrder(testUser.uid, any())).called(1); + verify(() => remoteCartRepository.setCart(testUser.uid, const Cart())); + }); + }); +} diff --git a/ecommerce_app/test/src/features/checkout/checkout_robot.dart b/ecommerce_app/test/src/features/checkout/checkout_robot.dart new file mode 100644 index 00000000..1af16439 --- /dev/null +++ b/ecommerce_app/test/src/features/checkout/checkout_robot.dart @@ -0,0 +1,26 @@ +import 'package:flutter_test/flutter_test.dart'; + +class CheckoutRobot { + CheckoutRobot(this.tester); + final WidgetTester tester; + + Future startCheckout() async { + final finder = find.text('Checkout'); + expect(finder, findsOneWidget); + await tester.tap(finder); + await tester.pumpAndSettle(); + } + + // payment + Future startPayment() async { + final finder = find.text('Pay'); + expect(finder, findsOneWidget); + await tester.tap(finder); + await tester.pumpAndSettle(); + } + + void expectPayButtonFound() { + final finder = find.text('Pay'); + expect(finder, findsOneWidget); + } +} diff --git a/ecommerce_app/test/src/features/checkout/presentation/checkout_screen/checkout_screen_test.dart b/ecommerce_app/test/src/features/checkout/presentation/checkout_screen/checkout_screen_test.dart new file mode 100644 index 00000000..a71fadc3 --- /dev/null +++ b/ecommerce_app/test/src/features/checkout/presentation/checkout_screen/checkout_screen_test.dart @@ -0,0 +1,35 @@ +import 'package:flutter_test/flutter_test.dart'; + +import '../../../../robot.dart'; + +void main() { + testWidgets('checkout when not previously signed in', (tester) async { + final r = Robot(tester); + await r.pumpMyApp(); + // add a product and start checkout + await r.products.selectProduct(); + await r.cart.addToCart(); + await r.cart.openCart(); + await r.checkout.startCheckout(); + // sign in from checkout screen + r.auth.expectEmailAndPasswordFieldsFound(); + await r.auth.signInWithEmailAndPassword(); + // check that we move to the payment page + r.checkout.expectPayButtonFound(); + }); + + testWidgets('checkout when previously signed in', (tester) async { + final r = Robot(tester); + await r.pumpMyApp(); + // sign in first + await r.auth.openEmailPasswordSignInScreen(); + await r.auth.signInWithEmailAndPassword(); + // then add a product and start checkout + await r.products.selectProduct(); + await r.cart.addToCart(); + await r.cart.openCart(); + await r.checkout.startCheckout(); + // expect that we see the payment page right away + r.checkout.expectPayButtonFound(); + }); +} 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 new file mode 100644 index 00000000..a69ce49f --- /dev/null +++ b/ecommerce_app/test/src/features/checkout/presentation/payment/payment_button_controller_test.dart @@ -0,0 +1,53 @@ +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'; +import 'package:mocktail/mocktail.dart'; + +import '../../../../mocks.dart'; + +void main() { + group('pay', () { + test('success', () async { + // setup + final checkoutService = MockCheckoutService(); + when(() => checkoutService.placeOrder()).thenAnswer( + (_) => Future.value(null), + ); + final controller = + PaymentButtonController(checkoutService: checkoutService); + // run & verify + expectLater( + controller.stream, + emitsInOrder([ + const AsyncLoading(), + const AsyncData(null), + ]), + ); + await controller.pay(); + }); + + test('failure', () async { + // setup + final checkoutService = MockCheckoutService(); + when(() => checkoutService.placeOrder()).thenThrow( + Exception('Card declined'), + ); + final controller = + PaymentButtonController(checkoutService: checkoutService); + // run & verify + expectLater( + controller.stream, + emitsInOrder([ + const AsyncLoading(), + predicate>( + (value) { + expect(value.hasError, true); + return true; + }, + ), + ]), + ); + await controller.pay(); + }); + }); +} diff --git a/ecommerce_app/test/src/features/orders/orders_robot.dart b/ecommerce_app/test/src/features/orders/orders_robot.dart new file mode 100644 index 00000000..e945ed1a --- /dev/null +++ b/ecommerce_app/test/src/features/orders/orders_robot.dart @@ -0,0 +1,17 @@ +import 'package:ecommerce_app/src/features/orders/presentation/orders_list/order_card.dart'; +import 'package:flutter_test/flutter_test.dart'; + +class OrdersRobot { + OrdersRobot(this.tester); + final WidgetTester tester; + + void expectFindZeroOrders() { + final finder = find.byType(OrderCard); + expect(finder, findsNothing); + } + + void expectFindNOrders(int count) { + final finder = find.byType(OrderCard); + expect(finder, findsNWidgets(count)); + } +} diff --git a/ecommerce_app/test/src/features/purchase_flow_test.dart b/ecommerce_app/test/src/features/purchase_flow_test.dart index efa40386..1310d6e0 100644 --- a/ecommerce_app/test/src/features/purchase_flow_test.dart +++ b/ecommerce_app/test/src/features/purchase_flow_test.dart @@ -13,15 +13,17 @@ void main() { await r.cart.addToCart(); await r.cart.openCart(); r.cart.expectFindNCartItems(1); - await r.closePage(); - // sign in - await r.openPopupMenu(); - await r.auth.openEmailPasswordSignInScreen(); + // checkout + await r.checkout.startCheckout(); await r.auth.signInWithEmailAndPassword(); - r.products.expectFindAllProductCards(); - // check cart again (to verify cart synchronization) - await r.cart.openCart(); r.cart.expectFindNCartItems(1); + await r.checkout.startPayment(); + // when a payment is complete, user is taken to the orders page + r.orders.expectFindNOrders(1); + await r.closePage(); // close orders page + // check that cart is now empty + await r.cart.openCart(); + r.cart.expectFindZeroCartItems(); await r.closePage(); // sign out await r.openPopupMenu(); diff --git a/ecommerce_app/test/src/mocks.dart b/ecommerce_app/test/src/mocks.dart index 1c5ea71f..84fca9da 100644 --- a/ecommerce_app/test/src/mocks.dart +++ b/ecommerce_app/test/src/mocks.dart @@ -2,6 +2,8 @@ import 'package:ecommerce_app/src/features/authentication/data/fake_auth_reposit import 'package:ecommerce_app/src/features/cart/application/cart_service.dart'; import 'package:ecommerce_app/src/features/cart/data/local/local_cart_repository.dart'; import 'package:ecommerce_app/src/features/cart/data/remote/remote_cart_repository.dart'; +import 'package:ecommerce_app/src/features/checkout/application/fake_checkout_service.dart'; +import 'package:ecommerce_app/src/features/orders/data/fake_orders_repository.dart'; import 'package:ecommerce_app/src/features/products/data/fake_products_repository.dart'; import 'package:mocktail/mocktail.dart'; @@ -14,3 +16,7 @@ class MockLocalCartRepository extends Mock implements LocalCartRepository {} class MockCartService extends Mock implements CartService {} class MockProductsRepository extends Mock implements FakeProductsRepository {} + +class MockOrdersRepository extends Mock implements FakeOrdersRepository {} + +class MockCheckoutService extends Mock implements FakeCheckoutService {} diff --git a/ecommerce_app/test/src/robot.dart b/ecommerce_app/test/src/robot.dart index 64be4d88..4fd8a1a1 100644 --- a/ecommerce_app/test/src/robot.dart +++ b/ecommerce_app/test/src/robot.dart @@ -12,6 +12,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'features/authentication/auth_robot.dart'; import 'features/cart/cart_robot.dart'; +import 'features/checkout/checkout_robot.dart'; +import 'features/orders/orders_robot.dart'; import 'features/products/products_robot.dart'; import 'goldens/golden_robot.dart'; @@ -20,11 +22,15 @@ class Robot { : auth = AuthRobot(tester), products = ProductsRobot(tester), cart = CartRobot(tester), + checkout = CheckoutRobot(tester), + orders = OrdersRobot(tester), golden = GoldenRobot(tester); final WidgetTester tester; final AuthRobot auth; final ProductsRobot products; final CartRobot cart; + final CheckoutRobot checkout; + final OrdersRobot orders; final GoldenRobot golden; Future pumpMyApp() async {