Checkout & Payment Testing
The Checkout Testing Challenge
Checkout is the most complex and critical part of e-commerce:
- Multi-step forms
- Real-time validation
- Address verification
- Shipping calculations
- Tax computations
- Payment processing
- Order confirmation
A bug here = lost revenue + angry customers + potential legal issues.
Checkout Flow Architecture
Each step in the checkout flow has specific validation requirements:
- Cart Review: Items list, subtotal, continue button
- Shipping Details: Address form, shipping options, tax calculation
- Payment Details: Card details, billing address, place order
- Confirmation: Order number, email confirmation, order summary
Checkout Page Object
# pages/checkout_page.py
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import Select
from pages.base_page import BasePage
class CheckoutPage(BasePage):
"""Multi-step checkout page"""
# Shipping Form
EMAIL = (By.ID, "checkout-email")
FIRST_NAME = (By.ID, "first-name")
LAST_NAME = (By.ID, "last-name")
ADDRESS = (By.ID, "address")
CITY = (By.ID, "city")
STATE = (By.ID, "state")
ZIP_CODE = (By.ID, "zip-code")
COUNTRY = (By.ID, "country")
PHONE = (By.ID, "phone")
# Shipping Options
SHIPPING_OPTIONS = (By.CSS_SELECTOR, "[data-testid='shipping-option']")
SHIPPING_STANDARD = (By.CSS_SELECTOR, "[data-testid='shipping-standard']")
SHIPPING_EXPRESS = (By.CSS_SELECTOR, "[data-testid='shipping-express']")
# Payment Form
CARD_NUMBER = (By.ID, "card-number")
CARD_EXPIRY = (By.ID, "card-expiry")
CARD_CVV = (By.ID, "card-cvv")
CARD_NAME = (By.ID, "card-name")
# Buttons
CONTINUE_TO_PAYMENT = (By.CSS_SELECTOR, "[data-testid='continue-payment']")
PLACE_ORDER = (By.CSS_SELECTOR, "[data-testid='place-order']")
# Order Summary
SUBTOTAL = (By.CSS_SELECTOR, "[data-testid='subtotal']")
SHIPPING_COST = (By.CSS_SELECTOR, "[data-testid='shipping-cost']")
TAX = (By.CSS_SELECTOR, "[data-testid='tax']")
TOTAL = (By.CSS_SELECTOR, "[data-testid='total']")
# Errors
FIELD_ERROR = (By.CSS_SELECTOR, ".field-error")
PAYMENT_ERROR = (By.CSS_SELECTOR, "[data-testid='payment-error']")
def fill_shipping_info(self, info: dict):
"""Fill shipping form with provided info"""
self.type(self.EMAIL, info.get("email", ""))
self.type(self.FIRST_NAME, info.get("first_name", ""))
self.type(self.LAST_NAME, info.get("last_name", ""))
self.type(self.ADDRESS, info.get("address", ""))
self.type(self.CITY, info.get("city", ""))
if info.get("state"):
Select(self.find(self.STATE)).select_by_visible_text(info["state"])
self.type(self.ZIP_CODE, info.get("zip_code", ""))
if info.get("country"):
Select(self.find(self.COUNTRY)).select_by_visible_text(info["country"])
self.type(self.PHONE, info.get("phone", ""))
return self
def select_shipping_option(self, option="standard"):
"""Select shipping method"""
if option == "standard":
self.click(self.SHIPPING_STANDARD)
elif option == "express":
self.click(self.SHIPPING_EXPRESS)
return self
def continue_to_payment(self):
"""Click continue to payment"""
self.click(self.CONTINUE_TO_PAYMENT)
# Wait for payment section to load
self.wait.until(EC.visibility_of_element_located(self.CARD_NUMBER))
return self
def fill_payment_info(self, info: dict):
"""Fill payment form"""
self.type(self.CARD_NUMBER, info.get("card_number", ""))
self.type(self.CARD_EXPIRY, info.get("expiry", ""))
self.type(self.CARD_CVV, info.get("cvv", ""))
self.type(self.CARD_NAME, info.get("name", ""))
return self
def place_order(self):
"""Submit order"""
self.click(self.PLACE_ORDER)
# Wait for confirmation page
self.wait_for_url_contains("/order-confirmation")
from pages.confirmation_page import ConfirmationPage
return ConfirmationPage(self.driver)
def get_order_summary(self):
"""Get order totals"""
return {
"subtotal": self._parse_price(self.SUBTOTAL),
"shipping": self._parse_price(self.SHIPPING_COST),
"tax": self._parse_price(self.TAX),
"total": self._parse_price(self.TOTAL),
}
def _parse_price(self, locator):
"""Convert price text to float"""
text = self.get_text(locator)
return float(text.replace("$", "").replace(",", ""))
def get_field_errors(self):
"""Get all field error messages"""
errors = self.find_all(self.FIELD_ERROR)
return [e.text for e in errors]
def has_payment_error(self):
"""Check if payment error is displayed"""
return self.is_visible(self.PAYMENT_ERROR)
Test Data
# utils/test_data.py
VALID_SHIPPING = {
"email": "test@example.com",
"first_name": "John",
"last_name": "Doe",
"address": "123 Test Street",
"city": "San Francisco",
"state": "California",
"zip_code": "94102",
"country": "United States",
"phone": "415-555-1234"
}
# Test card numbers (use payment provider's test cards)
VALID_CARD = {
"card_number": "4242424242424242", # Stripe test card
"expiry": "12/28",
"cvv": "123",
"name": "John Doe"
}
DECLINED_CARD = {
"card_number": "4000000000000002", # Stripe decline test
"expiry": "12/28",
"cvv": "123",
"name": "John Doe"
}
INVALID_SHIPPING = {
"email": "not-an-email",
"first_name": "",
"zip_code": "invalid"
}
Checkout Test Cases
# tests/test_checkout.py
import pytest
from pages.checkout_page import CheckoutPage
from pages.cart_page import CartPage
from utils.test_data import VALID_SHIPPING, VALID_CARD, DECLINED_CARD
class TestCheckoutFlow:
def test_complete_checkout_success(self, driver, cart_with_one_item):
"""
Given: User has item in cart
When: User completes checkout with valid info
Then: Order confirmation is displayed
"""
cart = cart_with_one_item
checkout = cart.proceed_to_checkout()
checkout.fill_shipping_info(VALID_SHIPPING)
checkout.select_shipping_option("standard")
checkout.continue_to_payment()
checkout.fill_payment_info(VALID_CARD)
confirmation = checkout.place_order()
assert confirmation.has_order_number()
assert confirmation.is_success()
def test_shipping_form_validation(self, driver, cart_with_one_item):
"""
Given: User is on shipping step
When: User submits empty form
Then: Validation errors are shown
"""
checkout = cart_with_one_item.proceed_to_checkout()
# Try to continue without filling form
checkout.click(checkout.CONTINUE_TO_PAYMENT)
errors = checkout.get_field_errors()
assert len(errors) > 0
assert any("email" in e.lower() for e in errors)
def test_invalid_email_format(self, driver, cart_with_one_item):
"""
Given: User enters invalid email
When: User tries to continue
Then: Email validation error is shown
"""
checkout = cart_with_one_item.proceed_to_checkout()
invalid_info = VALID_SHIPPING.copy()
invalid_info["email"] = "not-an-email"
checkout.fill_shipping_info(invalid_info)
checkout.click(checkout.CONTINUE_TO_PAYMENT)
errors = checkout.get_field_errors()
assert any("email" in e.lower() for e in errors)
def test_shipping_option_affects_total(self, driver, cart_with_one_item):
"""
Given: User selects standard shipping
When: User changes to express
Then: Total increases
"""
checkout = cart_with_one_item.proceed_to_checkout()
checkout.fill_shipping_info(VALID_SHIPPING)
checkout.select_shipping_option("standard")
standard_total = checkout.get_order_summary()["total"]
checkout.select_shipping_option("express")
express_total = checkout.get_order_summary()["total"]
assert express_total > standard_total
def test_payment_declined(self, driver, cart_with_one_item):
"""
Given: User enters declined card
When: User places order
Then: Payment error is displayed
And: Order is not placed
"""
checkout = cart_with_one_item.proceed_to_checkout()
checkout.fill_shipping_info(VALID_SHIPPING)
checkout.select_shipping_option("standard")
checkout.continue_to_payment()
checkout.fill_payment_info(DECLINED_CARD)
# Click place order
checkout.click(checkout.PLACE_ORDER)
assert checkout.has_payment_error()
# Should still be on checkout page
assert "/checkout" in driver.current_url
Payment Testing Best Practices
Never Use Real Cards
# Test card numbers by provider
# Stripe Test Cards
STRIPE_SUCCESS = "4242424242424242"
STRIPE_DECLINED = "4000000000000002"
STRIPE_INSUFFICIENT = "4000000000009995"
STRIPE_3DS_REQUIRED = "4000000000003220"
# PayPal Sandbox
PAYPAL_EMAIL = "sb-buyer@example.com"
PAYPAL_PASSWORD = "sandbox_password"
# Braintree Sandbox
BRAINTREE_SUCCESS = "4111111111111111"
BRAINTREE_DECLINED = "4000111111111115"
Test Payment Scenarios
class TestPaymentScenarios:
@pytest.mark.parametrize("card,expected", [
("4242424242424242", "success"),
("4000000000000002", "declined"),
("4000000000009995", "insufficient_funds"),
])
def test_payment_card_scenarios(self, driver, cart_with_one_item, card, expected):
"""Test various card scenarios"""
checkout = cart_with_one_item.proceed_to_checkout()
checkout.fill_shipping_info(VALID_SHIPPING)
checkout.continue_to_payment()
payment_info = VALID_CARD.copy()
payment_info["card_number"] = card
checkout.fill_payment_info(payment_info)
checkout.click(checkout.PLACE_ORDER)
if expected == "success":
assert "/confirmation" in driver.current_url
else:
assert checkout.has_payment_error()
def test_expired_card(self, driver, cart_with_one_item):
"""Test expired card shows error"""
checkout = cart_with_one_item.proceed_to_checkout()
checkout.fill_shipping_info(VALID_SHIPPING)
checkout.continue_to_payment()
expired_card = VALID_CARD.copy()
expired_card["expiry"] = "01/20" # Past date
checkout.fill_payment_info(expired_card)
# Should show validation error before submit
assert checkout.is_visible(checkout.CARD_EXPIRY_ERROR)
Order Confirmation Testing
# pages/confirmation_page.py
class ConfirmationPage(BasePage):
"""Order confirmation page"""
ORDER_NUMBER = (By.CSS_SELECTOR, "[data-testid='order-number']")
ORDER_TOTAL = (By.CSS_SELECTOR, "[data-testid='order-total']")
SUCCESS_MESSAGE = (By.CSS_SELECTOR, "[data-testid='success-message']")
ORDER_ITEMS = (By.CSS_SELECTOR, "[data-testid='order-item']")
SHIPPING_ADDRESS = (By.CSS_SELECTOR, "[data-testid='shipping-address']")
def has_order_number(self):
"""Check if order number is displayed"""
return self.is_visible(self.ORDER_NUMBER)
def get_order_number(self):
"""Get order number"""
return self.get_text(self.ORDER_NUMBER)
def is_success(self):
"""Check if success message is shown"""
return self.is_visible(self.SUCCESS_MESSAGE)
def get_order_total(self):
"""Get confirmed order total"""
text = self.get_text(self.ORDER_TOTAL)
return float(text.replace("$", "").replace(",", ""))
# tests/test_order_confirmation.py
class TestOrderConfirmation:
def test_confirmation_shows_correct_order_details(
self, driver, completed_order
):
"""
Given: Order was placed successfully
Then: Confirmation shows correct items and total
"""
confirmation, expected_total = completed_order
assert confirmation.has_order_number()
assert len(confirmation.get_order_number()) > 5
# Total should match checkout total
assert abs(confirmation.get_order_total() - expected_total) < 0.01
def test_confirmation_email_sent(self, driver, completed_order):
"""
Given: Order was placed
Then: Confirmation email is sent
Note: This requires email testing service (Mailosaur, etc.)
"""
confirmation, _ = completed_order
# This would integrate with email testing service
# email = mailosaur.get_latest_email("test@example.mailosaur.net")
# assert confirmation.get_order_number() in email.body
pass # Placeholder for email testing
Security Testing Considerations
class TestCheckoutSecurity:
def test_cannot_manipulate_price_via_url(self, driver, cart_with_one_item):
"""Verify price can't be changed via URL params"""
checkout = cart_with_one_item.proceed_to_checkout()
original_total = checkout.get_order_summary()["total"]
# Try to manipulate via URL
driver.get(driver.current_url + "?total=1.00")
# Total should remain unchanged
assert checkout.get_order_summary()["total"] == original_total
def test_cvv_not_visible_in_dom(self, driver, cart_with_one_item):
"""Verify CVV is masked/not stored in DOM"""
checkout = cart_with_one_item.proceed_to_checkout()
checkout.fill_shipping_info(VALID_SHIPPING)
checkout.continue_to_payment()
checkout.fill_payment_info(VALID_CARD)
# CVV should be type="password" or masked
cvv_field = checkout.find(checkout.CARD_CVV)
assert cvv_field.get_attribute("type") == "password" or \
"*" in cvv_field.get_attribute("value")
Key Takeaways
- Use test payment credentials - Never real cards
- Test the full flow - Not just happy path
- Validate calculations - Subtotal, shipping, tax, total
- Test error scenarios - Declined cards, validation errors
- Verify security - Price manipulation, sensitive data handling
Next Lesson
Let's explore Selenium 4: CDP & BiDi Protocolโmodern browser capabilities for network interception, performance testing, and more.