๐Ÿ”ฅ 0
โญ 0
Lesson 8 of 10 30 min +250 XP

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

Checkout Flow

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.

Testing the Shopping Cart