Skip to content

Commit

Permalink
Update unit, widget, integration tests
Browse files Browse the repository at this point in the history
  • Loading branch information
bizz84 committed May 15, 2024
1 parent 6b54a71 commit e39adb6
Show file tree
Hide file tree
Showing 11 changed files with 284 additions and 34 deletions.
16 changes: 9 additions & 7 deletions ecommerce_app/integration_test/purchase_flow_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<TypeError>()));
});

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()));
});
});
}
26 changes: 26 additions & 0 deletions ecommerce_app/test/src/features/checkout/checkout_robot.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import 'package:flutter_test/flutter_test.dart';

class CheckoutRobot {
CheckoutRobot(this.tester);
final WidgetTester tester;

Future<void> startCheckout() async {
final finder = find.text('Checkout');
expect(finder, findsOneWidget);
await tester.tap(finder);
await tester.pumpAndSettle();
}

// payment
Future<void> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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();
});
}
Original file line number Diff line number Diff line change
@@ -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<void>(),
const AsyncData<void>(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<void>(),
predicate<AsyncValue<void>>(
(value) {
expect(value.hasError, true);
return true;
},
),
]),
);
await controller.pay();
});
});
}
17 changes: 17 additions & 0 deletions ecommerce_app/test/src/features/orders/orders_robot.dart
Original file line number Diff line number Diff line change
@@ -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));
}
}
16 changes: 9 additions & 7 deletions ecommerce_app/test/src/features/purchase_flow_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
6 changes: 6 additions & 0 deletions ecommerce_app/test/src/mocks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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 {}
6 changes: 6 additions & 0 deletions ecommerce_app/test/src/robot.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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<void> pumpMyApp() async {
Expand Down

0 comments on commit e39adb6

Please sign in to comment.