Article 4: Test Automation Frameworks: From Simple Scripts to Production-Ready Solutions
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

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?
- Key Terms You Need to Know
- Evolution of Test Automation
- Automation Framework Architecture
- Playwright: The Modern Standard
- Cypress: Developer-Friendly Testing
- Selenium WebDriver: Advanced Patterns
- Karate: API Testing Powerhouse
- pytest: Python Testing Framework
- Real Project: E-commerce Test Framework
- Best Practices & Anti-Patterns
- Learning Resources
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:
- Open a browser automatically
- Click buttons, fill forms, navigate pages
- Check that everything works correctly
- 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 Testing | Automated Testing |
|---|---|
| 2 hours per test run | 5 minutes per test run |
| Human gets tired, makes mistakes | Robot never gets tired |
| Can test on 1 browser at a time | Can test on 5 browsers simultaneously |
| Expensive (human salary) | Cheap (runs on servers) |
| Boring and repetitive | Write 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
| Term | What it is | Analogy |
|---|---|---|
| Browser | Chrome, Firefox, Safari — programs to view websites | A window to look at the internet |
| DOM | Document Object Model — the structure of a webpage | The skeleton/blueprint of a webpage |
| Element | Any part of a page: button, input, link, image | A bone in the skeleton |
| Selector | Address to find an element | GPS coordinates to find a building |
Testing Terms
| Term | What it is | Example |
|---|---|---|
| Test Case | One specific thing to verify | ”User can login with correct password” |
| Test Suite | Group of related test cases | ”All login tests” |
| Assertion | Checking if something is true | ”Page title should be ‘Dashboard‘“ |
| Fixture | Pre-prepared test data | User with email “test@example.com” |
Code Terms
| Term | What it is | Example |
|---|---|---|
| async/await | Way to wait for operations to complete | Wait for page to load before clicking |
| Locator | Code that finds an element | page.locator('#login-button') |
| Page Object | Class representing a webpage | LoginPage, DashboardPage |
| Hook | Code that runs before/after tests | beforeEach, 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 Type | Syntax | Reliability |
|---|---|---|
| ID | #submit | ⭐⭐⭐ Good |
| Class | .btn.primary | ⭐⭐ Medium |
| data-testid | [data-testid="login-btn"] | ⭐⭐⭐⭐ Best! |
| Text | text=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:
| Advantage | Description |
|---|---|
| ✅ Auto-waiting | No more sleeps! |
| ✅ Multi-browser | Chromium, Firefox, WebKit |
| ✅ Parallel execution | Out of the box |
| ✅ Network interception | Mocking API responses |
| ✅ Excellent documentation | Easy to learn |
| ✅ Active support | Microsoft |
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 Structure | With Structure |
|---|---|
| 100 tests in one file | Tests organized by feature |
| Copy-paste everywhere | Reusable page objects |
| Hardcoded values | Config files |
| ”It works on my machine” | Works everywhere |
| Hard to find anything | Clear 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:
| Requirement | What it means | Why it matters |
|---|---|---|
| 🍎 WebKit support | Can test Safari browser | Apple products use Safari |
| ⚡ Parallel execution | Run tests simultaneously | 100 tests in 5 min instead of 50 min |
| 🛡️ Auto-waiting | No manual sleeps | Tests don’t randomly fail |
| 🔧 Modern API | async/await syntax | Clean, readable code |
| 📱 Mobile emulation | Test mobile views | Apps 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.
Why Cypress is Popular?
Cypress is especially loved by frontend developers because:
| Advantage | What it means | Why it’s cool |
|---|---|---|
| ✅ All-in-one | Everything included | No need to install 10 packages |
| ✅ Time Travel | See each step visually | Click any step to see the page state |
| ✅ Real-time Reloads | Auto-reload on code change | Save file → tests run automatically |
| ✅ Automatic Waiting | No .sleep() needed | Cypress waits for elements automatically |
| ✅ Network Control | Intercept/mock API calls | Test without real backend |
| ✅ Screenshots & Videos | Auto-recorded | See what happened when test failed |
Cypress vs Playwright: Quick Comparison
| Cypress | Playwright | |
|---|---|---|
| Best for | JavaScript/React teams | Cross-browser testing |
| Browsers | Chrome, Firefox, Edge | Chrome, Firefox, Safari |
| Safari | ❌ No | ✅ Yes |
| Multiple tabs | ❌ No | ✅ Yes |
| Language | JavaScript only | JS, 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
| Criteria | Cypress | Playwright |
|---|---|---|
| Language | JavaScript/TypeScript | JS/TS/Python/Java/C# |
| Browsers | Chrome, Firefox, Edge, Electron | Chromium, Firefox, WebKit |
| Speed | Fast | Very Fast |
| Parallelism | Paid (Cypress Cloud) | Free |
| iFrames | Limited support | Full support |
| Multiple tabs | Not supported | Supported |
| Mobile | Viewport only | WebKit (Safari) |
| Debugging | Excellent Time Travel | Trace Viewer |
| Community | Huge | Growing |
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:
Given url— “I’m connecting to this address”And path— “Specifically to this endpoint”And param— “With this parameter”When method GET— “When I send a GET request”Then status 200— “Then I expect status 200 (success)”And match— “And I check that the response matches”
Why Apple Requires Karate?
From job description:
“Experience with Karate” — because:
| Reason | Explanation |
|---|---|
| 📝 BDD syntax | Non-programmers (managers, analysts) can read tests |
| 🔥 API + Performance | Test functionality AND speed in one tool |
| ✅ Built-in assertions | No need for extra libraries for validation |
| ⚡ Parallel execution | Run hundreds of tests simultaneously |
| 🛠️ No Java coding | Write tests without knowing Java (runs on JVM) |
| 🔄 Data-driven | Easily test with multiple data sets |
HTTP Methods Explained
Before writing API tests, you need to understand HTTP methods:
| Method | What it does | Example | Restaurant Analogy |
|---|---|---|---|
GET | Retrieve data | Get user profile | ”Show me the menu” |
POST | Create new data | Create new user | ”I’d like to order this dish” |
PUT | Update entire record | Update user profile | ”Replace my entire order” |
PATCH | Partial update | Change email only | ”Just change the drink” |
DELETE | Remove data | Delete account | ”Cancel my order” |
HTTP Status Codes
Every API response has a status code:
| Code | Name | Meaning | When you see it |
|---|---|---|---|
200 | OK | Success | Request worked |
201 | Created | Created successfully | After POST |
204 | No Content | Success, but empty | After DELETE |
400 | Bad Request | Your mistake | Wrong format/data |
401 | Unauthorized | Need to login | Missing auth token |
403 | Forbidden | Access denied | No permission |
404 | Not Found | Doesn’t exist | Wrong URL/ID |
500 | Server Error | Server’s fault | Bug 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:
| Marker | Meaning | Example |
|---|---|---|
#string | Any string | "John", "test@email.com" |
#number | Any number | 42, 3.14 |
#boolean | true or false | true, false |
#array | Any array | [1, 2, 3] |
#object | Any object | {"key": "value"} |
#null | null value | null |
#notnull | Any non-null value | anything except null |
#present | Field exists | field is in response |
#notpresent | Field doesn’t exist | field is missing |
#regex | Matches pattern | '#regex \\d{3}' = 3 digits |
#uuid | Valid UUID | '550e8400-e29b-41d4-...' |
#ignore | Skip this field | don’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
| Criteria | Karate | Postman | REST Assured |
|---|---|---|---|
| Syntax | Gherkin (readable) | GUI/JavaScript | Java code |
| Learning curve | Easy | Very easy | Medium |
| Version control | Excellent | Limited | Excellent |
| CI/CD | Native | Needs Newman | Native |
| Performance testing | Built-in (Gatling) | Limited | Separate tool |
| Mocking | Built-in | Limited | Separate |
| Non-programmers | Can read/write | Can use GUI | Cannot |
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?
| Feature | What it means | Example |
|---|---|---|
| ✅ Simple Syntax | Just write functions | def test_login(): |
| ✅ Fixtures | Prepare data for tests | Create test user before test |
| ✅ Parametrization | Run same test with different data | Test login with 10 different users |
| ✅ 800+ Plugins | Extend functionality | HTML reports, parallel execution |
| ✅ Smart Assertions | Just use assert | assert 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
| Criteria | pytest | unittest | nose2 |
|---|---|---|---|
| Syntax | Simple functions | Class-based | Both |
| Fixtures | Powerful, flexible | setUp/tearDown | Limited |
| Parametrization | Built-in | Subtest | Plugin |
| Plugins | 800+ | Limited | Some |
| Assertions | Plain assert | self.assertEqual | Plain assert |
| Learning Curve | Easy | Medium | Easy |
| Community | Huge | Standard library | Declining |
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
- Playwright Official Channel — Official tutorials
- Automation Step by Step - Raghav Pal — ⭐⭐⭐⭐⭐ Best for beginners
- LambdaTest — Playwright + CI/CD
💻 Online Courses
- “Playwright: Web Automation Testing From Zero to Hero” — Artem Bondar (Udemy)
- Test Automation University - Playwright — 🆓 Free from Applitools
- “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:
-
Create a GitHub project with:
- Playwright tests (mandatory!)
- CI/CD integration
- Good README
- Test examples
-
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