Test Automation Frameworks: From Simple Scripts to Production-Ready Solutions

Complete guide to Playwright, Cypress, Selenium, Karate and pytest: exactly what Apple and other top companies require


QA Interview

You Are Here:

[✓] Article 1: QA Fundamentals
[✓] Article 2: QA Practice  
[✓] Article 3: DSA for QA
[→] Article 4: Automation Frameworks ← Currently reading
[ ] Article 5: CI/CD
[ ] Article 6: Performance Testing
[ ] Article 7: Working at Apple

Progress: 57% ✨

Remember the Apple Software Quality Engineer job posting? Here’s what they require:

“Experience working with Automation frameworks such as: Karate, Playwright or Selenium”

Not just “can run tests”, but building frameworks. In this article we’ll cover:

  • How to build production-ready automation frameworks
  • Playwright deep dive (modern standard)
  • Selenium advanced patterns (classic)
  • Karate for API testing (powerful tool)
  • Real project: E-commerce test framework from scratch

By the end of this article you’ll have a ready portfolio project that will impress interviewers at Apple, Google, Amazon.


Table of Contents


What is Test Automation?

The Problem: Manual Testing is Slow

Imagine you’re a tester at an e-commerce company. Every day, you need to verify:

  • ✅ Users can register
  • ✅ Users can login
  • ✅ Products display correctly
  • ✅ Shopping cart works
  • ✅ Checkout process works
  • ✅ Payment goes through

Manually, this takes 2+ hours every day. And you need to do this:

  • Before every release
  • On different browsers (Chrome, Firefox, Safari)
  • On different devices (desktop, mobile, tablet)

That’s 6+ hours of repetitive clicking every single day! 😱

The Solution: Let Robots Do It

Test Automation means writing programs that:

  1. Open a browser automatically
  2. Click buttons, fill forms, navigate pages
  3. Check that everything works correctly
  4. Report any problems

Simple analogy:

Instead of manually washing dishes every day, you buy a dishwasher. You still need to load dishes and press buttons, but the machine does the hard work.

Real comparison:

Manual TestingAutomated Testing
2 hours per test run5 minutes per test run
Human gets tired, makes mistakesRobot never gets tired
Can test on 1 browser at a timeCan test on 5 browsers simultaneously
Expensive (human salary)Cheap (runs on servers)
Boring and repetitiveWrite once, run forever

What is a Framework?

A Framework is a organized structure for your tests. Think of it like a toolbox:

  • Without framework: Tools scattered everywhere, hard to find what you need
  • With framework: Everything organized in labeled drawers

Framework provides:

  • 📁 Folder structure (where to put test files)
  • 🔧 Reusable functions (don’t repeat code)
  • ⚙️ Configuration (settings for different environments)
  • 📊 Reporting (HTML reports with screenshots)
  • 🔄 CI/CD integration (run tests automatically)

Key Terms You Need to Know

Before diving into frameworks, let’s understand essential terms:

Browser & DOM

TermWhat it isAnalogy
BrowserChrome, Firefox, Safari — programs to view websitesA window to look at the internet
DOMDocument Object Model — the structure of a webpageThe skeleton/blueprint of a webpage
ElementAny part of a page: button, input, link, imageA bone in the skeleton
SelectorAddress to find an elementGPS coordinates to find a building

Testing Terms

TermWhat it isExample
Test CaseOne specific thing to verify”User can login with correct password”
Test SuiteGroup of related test cases”All login tests”
AssertionChecking if something is true”Page title should be ‘Dashboard‘“
FixturePre-prepared test dataUser with email “test@example.com

Code Terms

TermWhat it isExample
async/awaitWay to wait for operations to completeWait for page to load before clicking
LocatorCode that finds an elementpage.locator('#login-button')
Page ObjectClass representing a webpageLoginPage, DashboardPage
HookCode that runs before/after testsbeforeEach, afterAll

Types of Selectors

How do we tell the automation “click THIS button”? With selectors:

<button id="submit" class="btn primary" data-testid="login-btn">
  Login
</button>
Selector TypeSyntaxReliability
ID#submit⭐⭐⭐ Good
Class.btn.primary⭐⭐ Medium
data-testid[data-testid="login-btn"]⭐⭐⭐⭐ Best!
Texttext=Login⭐⭐ Medium
XPath//button[@id="submit"]⭐ Fragile

Best Practice: Always use data-testid attributes — they don’t change when design changes!


Evolution of Test Automation

Why History Matters

Understanding how we got here helps you:

  • Appreciate modern tools
  • Understand legacy code at work
  • Make better tool choices

2010-2015: Selenium Era (The Beginning)

The Problem: Tests were written like raw commands, one after another.

// Old approach - fragile, slow
// Each line is a separate command to the browser

driver.findElement(By.id("username")).sendKeys("test@example.com");
// ^ Find element with id="username" and type email into it

driver.findElement(By.id("password")).sendKeys("password123");
// ^ Find password field and type password

driver.findElement(By.xpath("//button[@type='submit']")).click();
// ^ Find submit button and click it

Thread.sleep(5000); // 😱 Hard wait!
// ^ Wait 5 seconds and HOPE the page loads
// Problem: What if page loads in 1 second? Wasted 4 seconds!
// What if page takes 6 seconds? Test fails!

Issues with this approach:

  • Hard waits — guessing how long to wait
  • Fragile selectors — XPath breaks if HTML changes slightly
  • No structure — just a list of commands
  • Hard to maintain — copy-paste everywhere

2016-2020: Page Object Model Era

The Improvement: Organize code by pages, not by raw commands.

// Better, but still problematic
// Now we have a class representing the login page

class LoginPage {
    constructor(driver) {
        this.driver = driver;
    }
    
    async login(email, password) {
        // All login logic in one place
        await this.driver.findElement(By.id("email")).sendKeys(email);
        await this.driver.findElement(By.id("password")).sendKeys(password);
        await this.driver.findElement(By.id("submit")).click();
    }
}

// Using it in test:
const loginPage = new LoginPage(driver);
await loginPage.login("test@example.com", "password123");
// Cleaner! But still has timing issues...

Better, but still problems:

  • ✅ Code is organized
  • ✅ Reusable functions
  • ❌ Still has timing issues (flaky tests)
  • ❌ Complex setup required

2021-2026: Modern Automation (Playwright Era)

The Revolution: Smart auto-waiting, simple syntax, reliable tests.

// Modern approach - reliable, fast
// Playwright handles waiting automatically!

test('login with valid credentials', async ({ page }) => {
    // test() - defines a test case
    // async ({ page }) - Playwright gives us a browser page
    
    await page.goto('/login');
    // ^ Navigate to login page
    // Playwright waits for page to load automatically!
    
    await page.fill('[data-testid="email"]', 'test@example.com');
    // ^ Find email field and type into it
    // Playwright waits for element to be visible and ready!
    
    await page.fill('[data-testid="password"]', 'password123');
    // ^ Fill password
    
    await page.click('[data-testid="login-button"]');
    // ^ Click login button
    // Playwright waits for button to be clickable!
    
    await expect(page).toHaveURL(/dashboard/);
    // ^ Assert: URL should contain "dashboard"
    // Playwright automatically retries until true or timeout!
});

// No Thread.sleep! No flaky tests! 🎉

Why Playwright became the standard:

AdvantageDescription
✅ Auto-waitingNo more sleeps!
✅ Multi-browserChromium, Firefox, WebKit
✅ Parallel executionOut of the box
✅ Network interceptionMocking API responses
✅ Excellent documentationEasy to learn
✅ Active supportMicrosoft

Automation Framework Architecture

What is a Framework Architecture?

Think of building a house:

  • Without blueprints → chaos, things don’t fit together
  • With blueprints → every room has a purpose, everything is organized

Framework architecture is the blueprint for your test automation project.

Folder Structure Explained

automation-framework/

├── tests/                    # 📋 TEST CASES
│   │                         # All your actual tests live here
│   ├── e2e/                  # End-to-end tests (full user journeys)
│   │   └── checkout.spec.js  # Example: complete purchase flow
│   ├── api/                  # API tests (backend testing)
│   │   └── users.spec.js     # Example: user API endpoints
│   └── integration/          # Integration tests (parts working together)

├── pages/                    # 📄 PAGE OBJECTS
│   │                         # Each webpage = one class
│   ├── BasePage.js           # Common functionality all pages share
│   ├── LoginPage.js          # Login page: email, password, submit
│   └── DashboardPage.js      # Dashboard after login

├── fixtures/                 # 📊 TEST DATA
│   │                         # Pre-prepared data for tests
│   ├── users.json            # {"email": "test@example.com", ...}
│   └── products.json         # Product data for shopping tests

├── utils/                    # 🔧 HELPER FUNCTIONS
│   │                         # Reusable utility code
│   ├── database.js           # Database connections
│   ├── dataGenerator.js      # Generate random test data
│   └── logger.js             # Logging functions

├── config/                   # ⚙️ CONFIGURATION
│   │                         # Different settings for different environments
│   ├── dev.config.js         # Development environment URLs
│   ├── staging.config.js     # Staging environment URLs
│   └── prod.config.js        # Production (careful here!)

├── reports/                  # 📈 TEST REPORTS
│                             # HTML reports, screenshots, videos
├── screenshots/              # 📸 Auto-captured on test failure
├── videos/                   # 🎥 Video recordings of tests

└── playwright.config.js      # 🎯 MAIN CONFIG FILE
                              # Browsers, timeouts, parallelization

Why This Structure Matters

Without StructureWith Structure
100 tests in one fileTests organized by feature
Copy-paste everywhereReusable page objects
Hardcoded valuesConfig files
”It works on my machine”Works everywhere
Hard to find anythingClear folder organization

Key Principles (With Examples)

1. DRY (Don’t Repeat Yourself)

The Problem: Same code in multiple places = nightmare to maintain

// ❌ BAD - code duplication
// If login form changes, you need to update EVERY test!

test('test 1', async ({ page }) => {
    await page.goto('/login');
    await page.fill('#email', 'test@example.com');
    await page.fill('#password', 'password');
    await page.click('button[type="submit"]');
    // ... rest of test
});

test('test 2', async ({ page }) => {
    // Same login code again!
    await page.goto('/login');
    await page.fill('#email', 'test@example.com');
    await page.fill('#password', 'password');
    await page.click('button[type="submit"]');
    // ... rest of test
});

// Imagine 50 tests... 50 places to update! 😱
// ✅ GOOD - reusable page object
// Login logic in ONE place

class LoginPage {
    constructor(page) {
        this.page = page;
    }
    
    async login(email, password) {
        await this.page.goto('/login');
        await this.page.fill('#email', email);
        await this.page.fill('#password', password);
        await this.page.click('button[type="submit"]');
    }
}

// Now tests are clean:
test('test 1', async ({ page }) => {
    const loginPage = new LoginPage(page);
    await loginPage.login('test@example.com', 'password');
    // ... rest of test
});

test('test 2', async ({ page }) => {
    const loginPage = new LoginPage(page);
    await loginPage.login('test@example.com', 'password');
    // ... rest of test
});

// Form changes? Update LoginPage ONCE, all tests work! 🎉

2. Single Responsibility

Each class should do ONE thing:

// ✅ Each class handles one page
class LoginPage { 
    // ONLY login-related actions
    async login(email, password) { /* ... */ }
    async forgotPassword(email) { /* ... */ }
    async getErrorMessage() { /* ... */ }
}

class ProductPage { 
    // ONLY product-related actions
    async addToCart(productName) { /* ... */ }
    async getPrice(productName) { /* ... */ }
    async filterByCategory(category) { /* ... */ }
}

class CheckoutPage { 
    // ONLY checkout-related actions
    async fillShippingAddress(address) { /* ... */ }
    async selectPaymentMethod(method) { /* ... */ }
    async placeOrder() { /* ... */ }
}

3. Data-Driven Testing

Separate test logic from test data:

// fixtures/users.json
{
    "validUsers": [
        { "email": "admin@test.com", "password": "admin123", "role": "admin" },
        { "email": "user@test.com", "password": "user123", "role": "user" },
        { "email": "guest@test.com", "password": "guest123", "role": "guest" }
    ]
}
// test file
const testData = require('./fixtures/users.json');

// One test definition, runs 3 times with different data!
for (const user of testData.validUsers) {
    test(`login as ${user.role}`, async ({ page }) => {
        const loginPage = new LoginPage(page);
        await loginPage.login(user.email, user.password);
        // Test passes for admin, user, AND guest
    });
}

Playwright: The Modern Standard

What is Playwright?

Playwright is a tool by Microsoft that:

  • Opens browsers (Chrome, Firefox, Safari) automatically
  • Clicks buttons, fills forms, navigates pages
  • Checks that everything works correctly
  • Takes screenshots and videos

Simple analogy:

Playwright is like a robot that can use a computer. You give it instructions: “go to this website, click this button, check if this text appears” — and it does it faster than any human.

Why Apple Requires Playwright?

From real job description:

“Experience with Playwright” — because:

RequirementWhat it meansWhy it matters
🍎 WebKit supportCan test Safari browserApple products use Safari
⚡ Parallel executionRun tests simultaneously100 tests in 5 min instead of 50 min
🛡️ Auto-waitingNo manual sleepsTests don’t randomly fail
🔧 Modern APIasync/await syntaxClean, readable code
📱 Mobile emulationTest mobile viewsApps must work on phones

Installation (Step by Step)

Step 1: Make sure you have Node.js installed

# Check if Node.js is installed
node --version
# Should show something like: v18.17.0

# If not installed, download from: https://nodejs.org/

Step 2: Create a new project

# Create folder and enter it
mkdir my-automation-project
cd my-automation-project

# Initialize Playwright (follow the prompts)
npm init playwright@latest

Step 3: Answer the setup questions

✔ Do you want to use TypeScript or JavaScript? → JavaScript
✔ Where to put your end-to-end tests? → tests
✔ Add a GitHub Actions workflow? → Yes
✔ Install Playwright browsers? → Yes

# This installs Chrome, Firefox, and Safari browsers

Step 4: Your project is ready!

# Run the example tests
npx playwright test

# See the beautiful HTML report
npx playwright show-report

Understanding playwright.config.js

This is the brain of your project. Let’s break it down:

// playwright.config.js
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
    // 📁 Where are your test files?
    testDir: './tests',
    
    // ⏱️ How long before a test times out?
    timeout: 30 * 1000,  // 30 seconds per test
    
    // ✅ How long to wait for assertions?
    expect: {
        timeout: 5000     // 5 seconds to check something
    },
    
    // 🚀 Run tests in parallel?
    fullyParallel: true,  // Yes! Much faster
    
    // 🔄 Retry failed tests?
    retries: process.env.CI ? 2 : 0,
    // On CI server: retry 2 times
    // Locally: no retries (see failures immediately)
    
    // 📊 How to report results?
    reporter: [
        ['html'],  // Beautiful HTML report
        ['json', { outputFile: 'test-results.json' }]
    ],
    
    // 🌐 Common settings for all tests
    use: {
        baseURL: 'https://demo.playwright.dev',
        // ^ Base URL - so you can write '/login' instead of full URL
        
        trace: 'on-first-retry',
        // ^ Record traces when test fails and retries
        
        screenshot: 'only-on-failure',
        // ^ Take screenshot when test fails
        
        video: 'retain-on-failure',
        // ^ Record video when test fails
    },
    
    // 🖥️ Which browsers to test on?
    projects: [
        {
            name: 'chromium',
            use: { ...devices['Desktop Chrome'] },
        },
        {
            name: 'firefox',
            use: { ...devices['Desktop Firefox'] },
        },
        {
            name: 'webkit',
            use: { ...devices['Desktop Safari'] },
        },
        {
            name: 'Mobile Chrome',
            use: { ...devices['Pixel 5'] },
        },
        {
            name: 'Mobile Safari',
            use: { ...devices['iPhone 12'] },
        },
    ],
});

Page Object Model in Playwright

What is Page Object Model (POM)?

POM is a design pattern where each webpage gets its own class. This class contains:

  • Locators — how to find elements on the page
  • Actions — what you can do on the page (click, fill, etc.)
  • Assertions — how to verify the page is correct

Why use POM?

  • ✅ If UI changes, update ONE file
  • ✅ Tests are cleaner and more readable
  • ✅ Reuse code across multiple tests

pages/BasePage.js — The Parent Class:

// BasePage.js
// This is the "parent" class that all other pages inherit from
// It contains common functionality that ALL pages need

export class BasePage {
    constructor(page) {
        this.page = page;
        // 'page' is the browser page object from Playwright
    }

    async goto(path) {
        // Navigate to a URL
        await this.page.goto(path);
    }

    async waitForPageLoad() {
        // Wait until no more network requests
        await this.page.waitForLoadState('networkidle');
    }

    async takeScreenshot(name) {
        // Capture full page screenshot
        await this.page.screenshot({ 
            path: `screenshots/${name}.png`,
            fullPage: true 
        });
    }

    async scrollToBottom() {
        // Scroll to the bottom of the page
        await this.page.evaluate(() => {
            window.scrollTo(0, document.body.scrollHeight);
        });
    }
}

pages/LoginPage.js — A Specific Page:

// LoginPage.js
// This class represents the login page
// It extends BasePage to get common functionality

import { BasePage } from './BasePage';

export class LoginPage extends BasePage {
    constructor(page) {
        super(page);  // Call parent constructor
        
        // Define locators — how to find each element
        // Using data-testid is the best practice!
        this.emailInput = page.locator('[data-testid="email"]');
        this.passwordInput = page.locator('[data-testid="password"]');
        this.loginButton = page.locator('[data-testid="login-button"]');
        this.errorMessage = page.locator('[data-testid="error-message"]');
        this.rememberMeCheckbox = page.locator('[data-testid="remember-me"]');
    }

    async goto() {
        // Navigate to login page
        await this.page.goto('/login');
        await this.waitForPageLoad();
    }

    async login(email, password, rememberMe = false) {
        await this.emailInput.fill(email);
        await this.passwordInput.fill(password);
        
        if (rememberMe) {
            await this.rememberMeCheckbox.check();
        }
        
        await this.loginButton.click();
    }

    async getErrorMessage() {
        return await this.errorMessage.textContent();
    }

    async isLoginButtonEnabled() {
        return await this.loginButton.isEnabled();
    }

    async waitForError() {
        await this.errorMessage.waitFor({ state: 'visible' });
    }
}

Tests Using Page Objects

tests/auth/login.spec.js:

import { test, expect } from '@playwright/test';
import { LoginPage } from '../../pages/LoginPage';
import { DashboardPage } from '../../pages/DashboardPage';

test.describe('Login Functionality', () => {
    let loginPage;
    let dashboardPage;

    test.beforeEach(async ({ page }) => {
        loginPage = new LoginPage(page);
        dashboardPage = new DashboardPage(page);
        await loginPage.goto();
    });

    test('successful login with valid credentials', async ({ page }) => {
        await loginPage.login('test@example.com', 'SecurePass123!');
        
        await expect(page).toHaveURL(/dashboard/);
        await expect(dashboardPage.welcomeMessage).toBeVisible();
        
        const welcomeText = await dashboardPage.getWelcomeMessage();
        expect(welcomeText).toContain('Welcome');
    });

    test('login fails with invalid password', async () => {
        await loginPage.login('test@example.com', 'wrongpassword');
        
        await loginPage.waitForError();
        const errorMsg = await loginPage.getErrorMessage();
        expect(errorMsg).toContain('Invalid credentials');
    });

    test('login button disabled with empty fields', async () => {
        const isEnabled = await loginPage.isLoginButtonEnabled();
        expect(isEnabled).toBe(false);
    });
});

Advanced Playwright Techniques

1. Network Interception & Mocking

test('mock API response', async ({ page }) => {
    // Intercept API request
    await page.route('**/api/user/profile', async route => {
        await route.fulfill({
            status: 200,
            contentType: 'application/json',
            body: JSON.stringify({
                id: 1,
                name: 'Test User',
                email: 'test@example.com',
                premium: true
            })
        });
    });

    await page.goto('/profile');
    
    // Verify premium badge is displayed
    await expect(page.locator('[data-testid="premium-badge"]')).toBeVisible();
});

2. Handling Multiple Tabs

test('opens link in new tab', async ({ page, context }) => {
    await page.goto('/');
    
    // Wait for new page
    const [newPage] = await Promise.all([
        context.waitForEvent('page'),
        page.click('a[target="_blank"]')
    ]);

    await newPage.waitForLoadState();
    expect(newPage.url()).toContain('about');
});

3. Visual Regression Testing

test('homepage looks correct', async ({ page }) => {
    await page.goto('/');
    
    // Screenshot comparison
    await expect(page).toHaveScreenshot('homepage.png', {
        maxDiffPixels: 100
    });
});

Cypress: Developer-Friendly Testing

What is Cypress?

Cypress is a JavaScript-based testing tool that runs directly in the browser. Unlike Playwright/Selenium which control the browser from outside, Cypress runs inside the browser.

Simple analogy:

Playwright is like a remote-controlled robot operating your computer from outside. Cypress is like a person sitting at the computer, seeing exactly what you see.

Cypress is especially loved by frontend developers because:

AdvantageWhat it meansWhy it’s cool
✅ All-in-oneEverything includedNo need to install 10 packages
✅ Time TravelSee each step visuallyClick any step to see the page state
✅ Real-time ReloadsAuto-reload on code changeSave file → tests run automatically
✅ Automatic WaitingNo .sleep() neededCypress waits for elements automatically
✅ Network ControlIntercept/mock API callsTest without real backend
✅ Screenshots & VideosAuto-recordedSee what happened when test failed

Cypress vs Playwright: Quick Comparison

CypressPlaywright
Best forJavaScript/React teamsCross-browser testing
BrowsersChrome, Firefox, EdgeChrome, Firefox, Safari
Safari❌ No✅ Yes
Multiple tabs❌ No✅ Yes
LanguageJavaScript onlyJS, Python, Java, C#
Free parallelism❌ Paid feature✅ Free
Debugging⭐⭐⭐⭐⭐ Excellent⭐⭐⭐⭐ Great

Cypress Setup (Step by Step)

Step 1: Install Cypress

# Create new project (if needed)
mkdir cypress-project
cd cypress-project
npm init -y

# Install Cypress
npm install cypress --save-dev

Step 2: Open Cypress for the first time

npx cypress open

This opens a beautiful GUI where you can:

  • Choose E2E or Component testing
  • Select a browser
  • Create your first test

Step 3: Configure Cypress

cypress.config.js:

// cypress.config.js
const { defineConfig } = require('cypress');

module.exports = defineConfig({
    e2e: {
        // Base URL — so you write '/login' not 'http://localhost:3000/login'
        baseUrl: 'http://localhost:3000',
        
        // Browser window size
        viewportWidth: 1280,
        viewportHeight: 720,
        
        // Record video of tests
        video: true,
        
        // Take screenshot on failure
        screenshotOnRunFailure: true,
        
        // How long to wait for elements (10 seconds)
        defaultCommandTimeout: 10000,
        
        // Retry failed tests
        retries: {
            runMode: 2,    // In CI: retry 2 times
            openMode: 0    // In GUI: no retries
        },
    },
});

Writing Tests in Cypress

Understanding Cypress syntax:

// Cypress uses a "chaining" syntax
// Each command chains from the previous one

cy.visit('/login')              // Go to login page
  .get('[data-testid="email"]') // Find email input
  .type('test@example.com')     // Type into it
  .get('[data-testid="password"]')
  .type('password123')
  .get('[data-testid="submit"]')
  .click()                       // Click submit
  .url()                         // Get current URL
  .should('include', '/dashboard');  // Assert it includes 'dashboard'

Page Object Model in Cypress

cypress/support/pages/LoginPage.js:

// LoginPage.js for Cypress
// Note: Cypress uses 'getters' differently than Playwright

class LoginPage {
    // Selectors using getters
    // These return Cypress chainable objects
    get emailInput() {
        return cy.get('[data-testid="email"]');
    }

    get passwordInput() {
        return cy.get('[data-testid="password"]');
    }

    get loginButton() {
        return cy.get('[data-testid="login-button"]');
    }

    get errorMessage() {
        return cy.get('[data-testid="error-message"]');
    }

    // Actions
    visit() {
        cy.visit('/login');
        return this;  // Return 'this' for chaining
    }

    login(email, password) {
        this.emailInput.type(email);
        this.passwordInput.type(password);
        this.loginButton.click();
        return this;
    }

    // Assertions
    assertErrorMessage(message) {
        this.errorMessage
            .should('be.visible')       // Element is visible
            .and('contain', message);   // Contains expected text
        return this;
    }
}

// Export single instance (singleton pattern)
export default new LoginPage();

Tests with Cypress

cypress/e2e/auth/login.cy.js:

// Import the page object
import LoginPage from '../../support/pages/LoginPage';

// describe() groups related tests
describe('Login Functionality', () => {
    
    // beforeEach() runs before EACH test
    beforeEach(() => {
        LoginPage.visit();
    });

    it('should login successfully with valid credentials', () => {
        LoginPage.login('test@example.com', 'SecurePass123!');
        
        cy.url().should('include', '/dashboard');
        cy.get('[data-testid="welcome-message"]').should('be.visible');
    });

    it('should show error with invalid credentials', () => {
        LoginPage.login('test@example.com', 'wrongpassword');
        
        LoginPage.assertErrorMessage('Invalid credentials');
    });
});

Custom Commands

cypress/support/commands.js:

// Login via API (faster than UI)
Cypress.Commands.add('loginByAPI', (email, password) => {
    cy.request({
        method: 'POST',
        url: '/api/auth/login',
        body: { email, password }
    }).then((response) => {
        window.localStorage.setItem('authToken', response.body.token);
    });
});

// Login via UI
Cypress.Commands.add('loginByUI', (email, password) => {
    cy.visit('/login');
    cy.get('[data-testid="email"]').type(email);
    cy.get('[data-testid="password"]').type(password);
    cy.get('[data-testid="login-button"]').click();
    cy.url().should('include', '/dashboard');
});

Network Stubbing & Mocking

describe('Network Mocking', () => {
    it('should mock API response', () => {
        cy.intercept('GET', '/api/users/profile', {
            statusCode: 200,
            body: {
                id: 1,
                name: 'Mock User',
                email: 'mock@example.com',
                premium: true
            }
        }).as('getProfile');

        cy.visit('/profile');
        cy.wait('@getProfile');
        
        cy.get('[data-testid="premium-badge"]').should('be.visible');
    });

    it('should simulate network error', () => {
        cy.intercept('GET', '/api/products', {
            statusCode: 500,
            body: { error: 'Internal Server Error' }
        }).as('getProducts');

        cy.visit('/products');
        cy.wait('@getProducts');
        
        cy.get('[data-testid="error-message"]')
            .should('contain', 'Something went wrong');
    });
});

Cypress vs Playwright: Comparison

CriteriaCypressPlaywright
LanguageJavaScript/TypeScriptJS/TS/Python/Java/C#
BrowsersChrome, Firefox, Edge, ElectronChromium, Firefox, WebKit
SpeedFastVery Fast
ParallelismPaid (Cypress Cloud)Free
iFramesLimited supportFull support
Multiple tabsNot supportedSupported
MobileViewport onlyWebKit (Safari)
DebuggingExcellent Time TravelTrace Viewer
CommunityHugeGrowing

When to Choose Cypress?

Use Cypress if:

  • ✅ Your team is JavaScript/React/Vue developers
  • ✅ Need quick start and simplicity
  • ✅ Developer Experience is important
  • ✅ Testing Single Page Applications
  • ✅ Don’t need Safari support

Use Playwright if:

  • ✅ Need Safari/WebKit support
  • ✅ Need multiple tabs/windows
  • ✅ Need free parallelism
  • ✅ Project uses Python/Java/C#

Selenium WebDriver: Advanced Patterns

What is Selenium?

Selenium is the “grandfather” of browser automation. It was created in 2004 and is still widely used, especially in large companies with existing test suites.

Simple analogy:

Selenium is like a classic car. It still works great, millions of people drive it, and there are mechanics everywhere who know how to fix it. But newer cars (Playwright) have better features.

When to Use Selenium?

Use Selenium if:Use Playwright instead if:
✅ Company already uses Selenium✅ Starting a new project
✅ Need to test Internet Explorer✅ Need Safari support
✅ Team knows Java/Python/C#✅ Want auto-waiting
✅ Using Appium for mobile✅ Want easier setup
✅ Large existing test suite✅ Want parallel execution

Understanding Selenium Architecture

Your Test Code

Selenium WebDriver (translates commands)

Browser Driver (chromedriver, geckodriver, etc.)

Actual Browser (Chrome, Firefox, etc.)

Key difference from Playwright:

  • Selenium needs separate browser drivers
  • You must match driver version to browser version
  • More setup, more maintenance

Modern Selenium Setup

Step 1: Install dependencies

npm install selenium-webdriver chromedriver

Step 2: Create configuration

config/selenium.config.js:

// Selenium configuration file
const { Builder, Browser, By, until } = require('selenium-webdriver');
const chrome = require('selenium-webdriver/chrome');

class SeleniumConfig {
    static async getDriver() {
        // Chrome options
        const options = new chrome.Options();
        
        // Run without visible browser window (for CI/CD)
        if (process.env.HEADLESS === 'true') {
            options.addArguments('--headless=new');
        }
        
        // Required for running in Docker/CI
        options.addArguments('--no-sandbox');
        options.addArguments('--disable-dev-shm-usage');
        options.addArguments('--window-size=1920,1080');
        
        // Create the WebDriver instance
        const driver = await new Builder()
            .forBrowser(Browser.CHROME)
            .setChromeOptions(options)
            .build();
        
        // Set timeouts
        await driver.manage().setTimeouts({
            implicit: 10000,  // Wait up to 10s for elements
            pageLoad: 30000,  // Wait up to 30s for page load
            script: 30000     // Wait up to 30s for scripts
        });
        
        return driver;
    }
}

module.exports = SeleniumConfig;

Selenium Waits Explained

One of the trickiest parts of Selenium is waiting. There are three types:

// ❌ BAD: Hard wait (never use this!)
await driver.sleep(5000);  // Wait 5 seconds no matter what
// Problem: Wastes time if element appears faster

// ⚠️ OK: Implicit wait (set once, applies everywhere)
await driver.manage().setTimeouts({ implicit: 10000 });
// Problem: Can hide real issues, hard to debug

// ✅ GOOD: Explicit wait (best practice)
const { until } = require('selenium-webdriver');

// Wait for specific element to be clickable
const button = await driver.wait(
    until.elementLocated(By.id('submit')),
    10000,  // timeout
    'Submit button not found'  // error message
);
await driver.wait(until.elementIsVisible(button), 5000);
await button.click();

Selenium Locators

const { By } = require('selenium-webdriver');

// Different ways to find elements:
await driver.findElement(By.id('username'));
await driver.findElement(By.name('email'));
await driver.findElement(By.className('btn-primary'));
await driver.findElement(By.css('[data-testid="login"]'));
await driver.findElement(By.xpath('//button[text()="Submit"]'));
await driver.findElement(By.linkText('Click here'));

Page Object Model in Selenium

// pages/LoginPage.js
const { By, until } = require('selenium-webdriver');

class LoginPage {
    constructor(driver) {
        this.driver = driver;
        
        // Locators
        this.emailInput = By.css('[data-testid="email"]');
        this.passwordInput = By.css('[data-testid="password"]');
        this.submitButton = By.css('[data-testid="submit"]');
        this.errorMessage = By.css('[data-testid="error"]');
    }
    
    async navigate() {
        await this.driver.get('https://example.com/login');
    }
    
    async login(email, password) {
        // Find and fill email
        const emailEl = await this.driver.findElement(this.emailInput);
        await emailEl.clear();
        await emailEl.sendKeys(email);
        
        // Find and fill password
        const passwordEl = await this.driver.findElement(this.passwordInput);
        await passwordEl.clear();
        await passwordEl.sendKeys(password);
        
        // Click submit
        const submitEl = await this.driver.findElement(this.submitButton);
        await submitEl.click();
    }
    
    async getErrorMessage() {
        const errorEl = await this.driver.wait(
            until.elementLocated(this.errorMessage),
            5000
        );
        return await errorEl.getText();
    }
}

module.exports = LoginPage;

Karate: API Testing Powerhouse

What is Karate?

Karate is a tool for testing APIs (Application Programming Interface). While Playwright tests what users see (buttons, forms), Karate tests the “backend” — the data that a website sends and receives from servers.

Simple analogy:

Imagine a restaurant. Playwright is like testing the dining room (menu, tables, waiters). Karate is like testing the kitchen (are dishes prepared correctly? are the right ingredients delivered?).

What is an API?

Before diving into Karate, let’s understand what an API is:

API (Application Programming Interface) — is a way for programs to communicate with each other. When you open a weather app, it doesn’t measure the temperature itself — it asks a server: “What’s the weather in New York?” and receives a response.

Your App  ──────────►  Server
         "GET /weather?city=NYC"
         
Your App  ◄──────────  Server
         {"temp": 72, "condition": "sunny"}

Karate tests these requests and responses directly, without opening a browser.

Gherkin Syntax: Writing Tests in Plain English

Karate uses Gherkin — a way to write tests in almost human-readable language:

Feature: Weather API Testing
  Scenario: Get weather for New York
    Given url 'https://api.weather.com'        # Base URL
    And path 'weather'                          # Endpoint: /weather
    And param city = 'NYC'                      # Query parameter: ?city=NYC
    When method GET                             # Send GET request
    Then status 200                             # Expect success (200 OK)
    And match response.temp == '#number'        # Temperature should be a number

Translation to plain English:

  1. Given url — “I’m connecting to this address”
  2. And path — “Specifically to this endpoint”
  3. And param — “With this parameter”
  4. When method GET — “When I send a GET request”
  5. Then status 200 — “Then I expect status 200 (success)”
  6. And match — “And I check that the response matches”

Why Apple Requires Karate?

From job description:

“Experience with Karate” — because:

ReasonExplanation
📝 BDD syntaxNon-programmers (managers, analysts) can read tests
🔥 API + PerformanceTest functionality AND speed in one tool
✅ Built-in assertionsNo need for extra libraries for validation
⚡ Parallel executionRun hundreds of tests simultaneously
🛠️ No Java codingWrite tests without knowing Java (runs on JVM)
🔄 Data-drivenEasily test with multiple data sets

HTTP Methods Explained

Before writing API tests, you need to understand HTTP methods:

MethodWhat it doesExampleRestaurant Analogy
GETRetrieve dataGet user profile”Show me the menu”
POSTCreate new dataCreate new user”I’d like to order this dish”
PUTUpdate entire recordUpdate user profile”Replace my entire order”
PATCHPartial updateChange email only”Just change the drink”
DELETERemove dataDelete account”Cancel my order”

HTTP Status Codes

Every API response has a status code:

CodeNameMeaningWhen you see it
200OKSuccessRequest worked
201CreatedCreated successfullyAfter POST
204No ContentSuccess, but emptyAfter DELETE
400Bad RequestYour mistakeWrong format/data
401UnauthorizedNeed to loginMissing auth token
403ForbiddenAccess deniedNo permission
404Not FoundDoesn’t existWrong URL/ID
500Server ErrorServer’s faultBug on server

Karate Setup

pom.xml (Maven):

<dependency>
    <groupId>com.intuit.karate</groupId>
    <artifactId>karate-junit5</artifactId>
    <version>1.4.1</version>
    <scope>test</scope>
</dependency>

Or with Gradle:

testImplementation 'com.intuit.karate:karate-junit5:1.4.1'

Basic API Test Explained

src/test/java/features/users.feature:

Feature: User API Testing
# Feature — describes what we're testing

  Background:
  # Background — runs before EACH scenario (like beforeEach)
    * url 'https://api.example.com'
    # Set base URL for all requests
    * header Accept = 'application/json'
    # We want JSON responses

  Scenario: Get user by ID
  # Scenario — one test case
    Given path 'users', 1
    # Full URL becomes: https://api.example.com/users/1
    When method GET
    # Send GET request
    Then status 200
    # Expect 200 OK
    And match response == 
    # Check response structure
      """
      {
        id: 1,
        name: '#string',
        # '#string' means any string value
        email: '#regex ^.+@.+\\..+$',
        # '#regex' validates email format
        active: '#boolean'
        # '#boolean' means true or false
      }
      """

  Scenario: Create new user
    Given path 'users'
    And request 
    # Request body — data we're sending
      """
      {
        name: 'John Doe',
        email: 'john.doe@example.com',
        password: 'SecurePass123!'
      }
      """
    When method POST
    # POST = create new resource
    Then status 201
    # 201 = Created successfully
    And match response.id == '#number'
    # New user should have numeric ID
    And match response.name == 'John Doe'
    # Name should match what we sent

Karate Match Expressions

Karate has special markers for flexible matching:

MarkerMeaningExample
#stringAny string"John", "test@email.com"
#numberAny number42, 3.14
#booleantrue or falsetrue, false
#arrayAny array[1, 2, 3]
#objectAny object{"key": "value"}
#nullnull valuenull
#notnullAny non-null valueanything except null
#presentField existsfield is in response
#notpresentField doesn’t existfield is missing
#regexMatches pattern'#regex \\d{3}' = 3 digits
#uuidValid UUID'550e8400-e29b-41d4-...'
#ignoreSkip this fielddon’t validate

Data-Driven Testing

Test with multiple data sets:

test-data.json:

[
  { "name": "User 1", "email": "user1@test.com", "valid": true },
  { "name": "User 2", "email": "invalid-email", "valid": false },
  { "name": "", "email": "user3@test.com", "valid": false }
]

users-validation.feature:

Feature: User Validation

  Background:
    * url 'https://api.example.com'

  Scenario Outline: Validate user creation: <name>
    Given path 'users'
    And request { name: '<name>', email: '<email>' }
    When method POST
    Then status <expectedStatus>

    Examples:
      | name   | email            | expectedStatus |
      | John   | john@test.com    | 201            |
      | Jane   | invalid-email    | 400            |
      |        | empty@test.com   | 400            |

Authentication Testing

Feature: Authentication

  Background:
    * url 'https://api.example.com'

  Scenario: Login and use token
    # Step 1: Login
    Given path 'auth/login'
    And request { email: 'test@example.com', password: 'SecurePass123!' }
    When method POST
    Then status 200
    And match response.token == '#string'
    
    # Save token for later use
    * def authToken = response.token

    # Step 2: Use token for protected endpoint
    Given path 'users/me'
    And header Authorization = 'Bearer ' + authToken
    When method GET
    Then status 200
    And match response.email == 'test@example.com'

Reusable Functions

common.feature:

@ignore
Feature: Reusable Functions

  Scenario: Login
    Given url baseUrl
    And path 'auth/login'
    And request { email: '#(email)', password: '#(password)' }
    When method POST
    Then status 200
    * def token = response.token

Use in other tests:

Feature: Orders API

  Background:
    * url 'https://api.example.com'
    * def login = call read('common.feature@login') { email: 'test@example.com', password: 'pass123' }
    * def token = login.token

  Scenario: Get my orders
    Given path 'orders'
    And header Authorization = 'Bearer ' + token
    When method GET
    Then status 200

Running Karate Tests

# Run all tests
mvn test

# Run specific feature file
mvn test -Dkarate.options="classpath:features/users.feature"

# Run with tags
mvn test -Dkarate.options="--tags @smoke"

# Generate HTML report
mvn test -Dkarate.options="--format html"

Karate vs Other API Tools

CriteriaKaratePostmanREST Assured
SyntaxGherkin (readable)GUI/JavaScriptJava code
Learning curveEasyVery easyMedium
Version controlExcellentLimitedExcellent
CI/CDNativeNeeds NewmanNative
Performance testingBuilt-in (Gatling)LimitedSeparate tool
MockingBuilt-inLimitedSeparate
Non-programmersCan read/writeCan use GUICannot

pytest: Python Testing Framework

What is pytest?

pytest is the most popular testing framework for Python. If Playwright is for JavaScript, pytest is for Python developers.

Simple analogy:

pytest is like a test organizer. It finds your tests, runs them, and tells you what passed or failed. Think of it as a teacher who automatically grades your homework.

Why Use pytest?

FeatureWhat it meansExample
✅ Simple SyntaxJust write functionsdef test_login():
✅ FixturesPrepare data for testsCreate test user before test
✅ ParametrizationRun same test with different dataTest login with 10 different users
✅ 800+ PluginsExtend functionalityHTML reports, parallel execution
✅ Smart AssertionsJust use assertassert 2 + 2 == 4

pytest vs unittest (Python’s built-in)

# unittest (built-in, verbose)
import unittest

class TestMath(unittest.TestCase):
    def test_addition(self):
        self.assertEqual(2 + 2, 4)
        
if __name__ == '__main__':
    unittest.main()

# pytest (simple, clean)
def test_addition():
    assert 2 + 2 == 4
    
# That's it! No class needed, no imports needed for basic tests

Installation (Step by Step)

Step 1: Install pytest

# Basic pytest
pip install pytest

# Common useful plugins
pip install pytest-html      # HTML reports
pip install pytest-xdist     # Parallel execution
pip install pytest-cov       # Code coverage
pip install pytest-playwright # Playwright integration

Step 2: Create configuration file

pytest.ini:

[pytest]
# Where to look for tests
testpaths = tests

# File patterns to find tests
python_files = test_*.py
# Files starting with "test_" are test files

# Class patterns
python_classes = Test*
# Classes starting with "Test" are test classes

# Function patterns
python_functions = test_*
# Functions starting with "test_" are test functions

# Command line options to always use
addopts = -v --tb=short --strict-markers
# -v = verbose output
# --tb=short = short traceback on failure
# --strict-markers = don't allow undefined markers

# Custom markers (tags for tests)
markers =
    smoke: Quick smoke tests (run often)
    regression: Full regression tests (run before release)
    api: API tests
    ui: UI tests

Project Structure Explained

pytest-framework/

├── tests/                    # 📋 ALL TESTS GO HERE
│   ├── conftest.py          # 🔧 Shared fixtures (available to all tests)
│   ├── test_login.py        # Tests for login feature
│   ├── test_api.py          # API tests
│   │
│   ├── api/                  # API test folder
│   │   ├── conftest.py      # API-specific fixtures
│   │   └── test_users.py    # User API tests
│   │
│   └── ui/                   # UI test folder
│       ├── conftest.py      # UI-specific fixtures
│       └── test_checkout.py # Checkout UI tests

├── pages/                    # 📄 Page Object Model
│   ├── base_page.py         # Common page functions
│   └── login_page.py        # Login page class

├── utils/                    # 🔧 Helper utilities
│   ├── api_client.py        # API helper functions
│   └── data_generator.py    # Generate test data

├── fixtures/                 # 📊 Test data files
│   └── test_data.json       # JSON data for tests

├── pytest.ini               # ⚙️ pytest configuration
└── requirements.txt         # 📦 Python dependencies

Writing Your First Tests

tests/test_basic.py:

# Simple test functions - no class needed!

def test_addition():
    """Test that addition works."""
    assert 2 + 2 == 4

def test_subtraction():
    """Test that subtraction works."""
    result = 10 - 3
    assert result == 7, f"Expected 7, got {result}"
    # ^ Custom error message

def test_list_contains():
    """Test that list contains expected item."""
    fruits = ["apple", "banana", "cherry"]
    assert "banana" in fruits

Run tests:

pytest                           # Run all tests
pytest -v                        # Verbose output
pytest tests/test_basic.py       # Run specific file
pytest -k "addition"             # Run tests with "addition" in name

Understanding Fixtures

What are fixtures?

Fixtures are functions that prepare something for your tests. They run BEFORE the test.

# tests/conftest.py
import pytest

@pytest.fixture
def sample_user():
    """Fixture that provides a test user."""
    return {
        "email": "test@example.com",
        "password": "SecurePass123!",
        "name": "Test User"
    }

@pytest.fixture
def empty_shopping_cart():
    """Fixture that provides an empty cart."""
    return []
# tests/test_user.py

def test_user_has_email(sample_user):
    """Test uses the sample_user fixture."""
    # pytest automatically calls sample_user() and passes result
    assert sample_user["email"] == "test@example.com"

def test_cart_is_empty(empty_shopping_cart):
    """Test uses the empty_shopping_cart fixture."""
    assert len(empty_shopping_cart) == 0

Fixture Scopes

import pytest

@pytest.fixture(scope="function")  # Default - runs before EACH test
def fresh_page():
    return create_new_page()

@pytest.fixture(scope="class")  # Runs once per test CLASS
def browser():
    return launch_browser()

@pytest.fixture(scope="module")  # Runs once per test FILE
def database_connection():
    return connect_to_database()

@pytest.fixture(scope="session")  # Runs once per TEST SESSION (all tests)
def app_config():
    return load_config()

Writing Tests with Classes

tests/test_login.py:

import pytest

class TestLogin:
    """Group related login tests together."""
    
    def test_successful_login(self, login_page, valid_user):
        """User can login with valid credentials."""
        # login_page and valid_user are fixtures (defined in conftest.py)
        login_page.login(valid_user.email, valid_user.password)
        
        assert login_page.is_logged_in()
        assert login_page.get_welcome_message() == f"Welcome, {valid_user.name}"
    
    def test_login_fails_with_invalid_password(self, login_page, valid_user):
        """Login fails with incorrect password."""
        login_page.login(valid_user.email, "wrongpassword")
        
        assert login_page.get_error_message() == "Invalid credentials"
        assert not login_page.is_logged_in()
    
    @pytest.mark.parametrize("email,password,error", [
        ("", "password", "Email is required"),
        ("test@example.com", "", "Password is required"),
        ("invalid-email", "password", "Invalid email format"),
    ])
    def test_login_validation(self, login_page, email, password, error):
        """Validate login form error messages."""
        login_page.login(email, password)
        
        assert login_page.get_error_message() == error

Fixtures (Dependency Injection)

tests/conftest.py:

import pytest
from dataclasses import dataclass
from playwright.sync_api import sync_playwright


@dataclass
class User:
    email: str
    password: str
    name: str


@pytest.fixture(scope="session")
def browser():
    """Create browser instance for entire test session."""
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        yield browser
        browser.close()


@pytest.fixture(scope="function")
def page(browser):
    """Create new page for each test."""
    context = browser.new_context()
    page = context.new_page()
    yield page
    context.close()


@pytest.fixture
def login_page(page):
    """Initialize LoginPage object."""
    from pages.login_page import LoginPage
    return LoginPage(page)


@pytest.fixture
def valid_user():
    """Return valid test user."""
    return User(
        email="test@example.com",
        password="SecurePass123!",
        name="Test User"
    )


@pytest.fixture
def api_client():
    """Create API client with authentication."""
    from utils.api_client import APIClient
    client = APIClient(base_url="https://api.example.com")
    client.authenticate()
    yield client
    client.close()

API Testing with pytest

tests/api/test_users.py:

import pytest
import requests


class TestUserAPI:
    """User API endpoint tests."""
    
    BASE_URL = "https://api.example.com"
    
    def test_get_user_by_id(self, api_client):
        """GET /users/{id} returns user details."""
        response = api_client.get("/users/1")
        
        assert response.status_code == 200
        assert response.json()["id"] == 1
        assert "email" in response.json()
    
    def test_create_user(self, api_client):
        """POST /users creates new user."""
        user_data = {
            "name": "John Doe",
            "email": "john.doe@example.com",
            "password": "SecurePass123!"
        }
        
        response = api_client.post("/users", json=user_data)
        
        assert response.status_code == 201
        assert response.json()["name"] == user_data["name"]
        assert "id" in response.json()
    
    @pytest.mark.parametrize("user_id,expected_status", [
        (1, 200),
        (999, 404),
        ("invalid", 400),
    ])
    def test_get_user_status_codes(self, api_client, user_id, expected_status):
        """Verify correct status codes for different user IDs."""
        response = api_client.get(f"/users/{user_id}")
        
        assert response.status_code == expected_status

Advanced pytest Features

1. Custom Markers

import pytest

@pytest.mark.smoke
def test_homepage_loads(page):
    """Quick smoke test for homepage."""
    page.goto("/")
    assert page.title() == "Home"

@pytest.mark.regression
@pytest.mark.slow
def test_complete_checkout_flow(page):
    """Full checkout regression test."""
    # ... complete flow
    pass

# Run only smoke tests:
# pytest -m smoke

# Run everything except slow tests:
# pytest -m "not slow"

2. Parametrized Fixtures

@pytest.fixture(params=["chrome", "firefox", "webkit"])
def browser_type(request):
    """Run tests on multiple browsers."""
    return request.param

def test_cross_browser(browser_type, page):
    """Test runs on all browsers."""
    page.goto("/")
    assert page.title() == "Home"

3. pytest with Playwright

import pytest
from playwright.sync_api import Page, expect


def test_login_with_playwright(page: Page):
    """Playwright integration with pytest."""
    page.goto("/login")
    
    page.fill('[data-testid="email"]', 'test@example.com')
    page.fill('[data-testid="password"]', 'password123')
    page.click('[data-testid="login-button"]')
    
    expect(page).to_have_url("/dashboard")
    expect(page.locator('[data-testid="welcome"]')).to_be_visible()

Running pytest

# Run all tests
pytest

# Run with verbose output
pytest -v

# Run specific test file
pytest tests/test_login.py

# Run specific test
pytest tests/test_login.py::TestLogin::test_successful_login

# Run tests matching keyword
pytest -k "login and not invalid"

# Run marked tests
pytest -m smoke

# Run in parallel (4 workers)
pytest -n 4

# Generate HTML report
pytest --html=reports/report.html

# Run with coverage
pytest --cov=src --cov-report=html

pytest vs Other Frameworks

Criteriapytestunittestnose2
SyntaxSimple functionsClass-basedBoth
FixturesPowerful, flexiblesetUp/tearDownLimited
ParametrizationBuilt-inSubtestPlugin
Plugins800+LimitedSome
AssertionsPlain assertself.assertEqualPlain assert
Learning CurveEasyMediumEasy
CommunityHugeStandard libraryDeclining

Real Project: E-commerce Test Framework

Project Description

What we test: Demo e-commerce site

Functionality:

  • ✅ User authentication
  • ✅ Product browsing
  • ✅ Shopping cart
  • ✅ Checkout process
  • ✅ Order management

Stack:

  • Playwright for UI
  • Karate for API
  • GitHub Actions for CI/CD

Complete E2E Test

tests/e2e/complete-purchase.spec.js:

import { test, expect } from '@playwright/test';
import { LoginPage } from '../../pages/LoginPage';
import { ProductsPage } from '../../pages/ProductsPage';
import { CartPage } from '../../pages/CartPage';
import { CheckoutPage } from '../../pages/CheckoutPage';

test.describe('Complete Purchase Flow', () => {
    test('user can complete full purchase', async ({ page }) => {
        const loginPage = new LoginPage(page);
        const productsPage = new ProductsPage(page);
        const cartPage = new CartPage(page);
        const checkoutPage = new CheckoutPage(page);

        // 1. Login
        await loginPage.goto();
        await loginPage.login('test@example.com', 'password');

        // 2. Browse and add products
        await productsPage.goto();
        await productsPage.addToCart('MacBook Pro');

        // 3. Checkout
        await cartPage.goto();
        await cartPage.proceedToCheckout();
        await checkoutPage.fillShippingInfo(testData.shipping);
        await checkoutPage.placeOrder();

        // 4. Verify confirmation
        const isConfirmed = await checkoutPage.isOrderConfirmed();
        expect(isConfirmed).toBe(true);
    });
});

Best Practices & Anti-Patterns

✅ Best Practices

1. Use data-testid attributes

<!-- ✅ Good -->
<button data-testid="submit-button">Submit</button>

<!-- ❌ Bad - fragile selector -->
<button class="btn btn-primary btn-lg">Submit</button>

2. Avoid hard timeouts

// ❌ Bad
await page.waitForTimeout(5000);
await page.click('[data-testid="button"]');

// ✅ Good
await page.waitForSelector('[data-testid="button"]', { state: 'visible' });
await page.click('[data-testid="button"]');

3. Isolate tests

// ✅ Good - each test is independent
test.beforeEach(async ({ page }) => {
    // Clean state for each test
    await page.goto('/');
    await clearDatabase();
    await seedTestData();
});

4. Meaningful test names

// ❌ Bad
test('test1', async () => { /* ... */ });

// ✅ Good
test('should login successfully with valid credentials', async () => { /* ... */ });

Learning Resources

YouTube Channels

  1. Playwright Official Channel — Official tutorials
  2. Automation Step by Step - Raghav Pal — ⭐⭐⭐⭐⭐ Best for beginners
  3. LambdaTest — Playwright + CI/CD

💻 Online Courses

  1. “Playwright: Web Automation Testing From Zero to Hero” — Artem Bondar (Udemy)
  2. Test Automation University - Playwright — 🆓 Free from Applitools
  3. “Selenium WebDriver with Java” — Rahul Shetty (Udemy)

🛠️ Practice Platforms

Demo Sites:

API Practice:


Interview Readiness Checklist

Playwright:

  • Can explain Page Object Model
  • Know how to use fixtures
  • Understand how locators work
  • Understand auto-waiting
  • Can set up CI/CD
  • Have written at least 50 tests
  • Have a project on GitHub

Selenium:

  • Know explicit vs implicit waits
  • Can work with WebDriverWait
  • Understand cross-browser testing

Karate:

  • Can write API tests
  • Understand data-driven testing
  • Know how to work with auth

pytest:

  • Understand fixture scope and dependency injection
  • Can use parametrization for data-driven tests
  • Know how to create custom markers
  • Can integrate with Playwright or Selenium
  • Understand conftest.py organization
  • Can run parallel tests with pytest-xdist

What’s Next?

In the next article we’ll cover CI/CD for QA Engineers:

  • Jenkins setup for test automation
  • GitHub Actions workflows
  • Docker for tests
  • Test reporting & dashboards

Next Article: Article 5: CI/CD for QA


Final Advice

To get into Apple:

  1. Create a GitHub project with:

    • Playwright tests (mandatory!)
    • CI/CD integration
    • Good README
    • Test examples
  2. Prepare a demo:

    • Screen recording of tests
    • Framework presentation
    • Architecture explanation

Remember: Apple is not looking for people who can just run tests, but engineers who can design test frameworks.


Tags: #Playwright #Selenium #Karate #TestAutomation #Apple #SDET #QA