Article 5: CI/CD for QA Engineers: From Manual Testing to Continuous Testing
CI/CD for QA Engineers: From Manual Testing to Continuous Testing
Complete guide to Jenkins, GitHub Actions, Docker and test automation integration in CI/CD pipeline

📍 You Are Here:
[✓] Article 1: QA Fundamentals
[✓] Article 2: QA Practice
[✓] Article 3: DSA for QA
[✓] Article 4: Automation Frameworks
[→] Article 5: CI/CD ← Currently reading
[ ] Article 6: Performance Testing
[ ] Article 7: Working at Apple
Progress: 71% ✨
“We have 500 automated tests, but we run them manually once a week” — hearing this in an interview, I realized the company was stuck in 2015.
In 2026, continuous testing is not an option, but a necessity. Apple, Google, Amazon deploy changes dozens of times a day. How do they do it? CI/CD pipelines with integrated testing.
From a real Apple job posting:
“Experience with CI/CD pipelines and test automation integration”
In this article you will learn:
- ✅ Setting up Jenkins for test automation
- ✅ Creating GitHub Actions workflows
- ✅ Containerizing tests with Docker
- ✅ Integrating Playwright/Selenium into pipeline
- ✅ Setting up notifications (Slack, Teams)
- ✅ Creating test dashboards
By the end of this article you’ll have a production-ready CI/CD pipeline for your portfolio.
📋 Table of Contents
- What is CI/CD and Why is it Important for QA
- Git & GitHub for QA
- GitHub Actions: Automation from Scratch
- Jenkins: Enterprise-level CI/CD
- Docker for Test Automation
- Integrating Tests into Pipeline
- Test Reporting & Dashboards
- Notifications & Monitoring
- Real Project: Complete CI/CD Setup
- Learning Resources
🎯 What is CI/CD and Why is it Important for QA?
Definitions
CI (Continuous Integration):
- Developers commit code frequently (several times a day)
- Each commit is automatically built
- Automated tests run with each commit
- Fast feedback (< 10 minutes)
CD (Continuous Delivery/Deployment):
- Automatic delivery to staging/production
- After passing all tests
- Minimal manual intervention
Traditional Process (without CI/CD)
Day 1: Developer writes code
Day 2-3: Code sits in branch
Day 4: Pull Request created
Day 5: Code review
Day 6: Merge to main
Day 7: QA manually runs tests
Day 8: Bugs found
Day 9: Fixes
Day 10: Release
Result: 10 days, lots of manual work, late bug discovery
With CI/CD
Minute 0: Developer pushes code
Minute 1: CI automatically:
- Builds project
- Runs unit tests
- Runs integration tests
- Runs E2E tests
Minute 10: Results ready
✅ All tests passed → automatic deploy to staging
❌ Tests failed → Slack notification
Result: 10 minutes, automation, immediate feedback
Role of QA in CI/CD
Traditional QA Role:
- ❌ Waits for ready builds
- ❌ Runs tests manually
- ❌ Searches for bugs after development
Modern QA Engineer (SDET):
- ✅ Creates automated tests
- ✅ Integrates tests into CI/CD
- ✅ Monitors test stability
- ✅ Shift-left testing (testing in early stages)
🌳 Git & GitHub for QA
Basic Git Commands
Setup:
# User configuration
git config --global user.name "Your Name"
git config --global user.email "your.email@example.com"
# Initialize repository
git init
# Clone existing
git clone https://github.com/username/repo.git
Daily work:
# Check status
git status
# Add files
git add . # All files
git add tests/login.spec.js # Specific file
# Commit
git commit -m "Add login tests"
# Push to GitHub
git push origin main
# Get changes
git pull origin main
Working with branches:
# Create new branch
git checkout -b feature/add-payment-tests
# Switch between branches
git checkout main
git checkout feature/add-payment-tests
# List branches
git branch
# Delete branch
git branch -d feature/add-payment-tests
# Merge branch
git checkout main
git merge feature/add-payment-tests
Git Workflow for QA
Feature Branch Workflow:
# 1. Create branch for new tests
git checkout -b feature/checkout-tests
# 2. Write tests
# tests/checkout.spec.js
# 3. Commit frequently
git add tests/checkout.spec.js
git commit -m "Add checkout validation tests"
# 4. Push to GitHub
git push origin feature/checkout-tests
# 5. Create Pull Request on GitHub
# 6. After review - merge to main
Commit Message Best Practices:
# ❌ Bad
git commit -m "updates"
git commit -m "fix"
git commit -m "test"
# ✅ Good
git commit -m "Add login tests for valid/invalid credentials"
git commit -m "Fix flaky test in checkout flow"
git commit -m "Update test data for payment methods"
git commit -m "Refactor Page Objects to use fixtures"
Conventional Commits for QA:
# Format: <type>: <description>
git commit -m "test: add E2E tests for user registration"
git commit -m "fix: resolve timeout issue in API tests"
git commit -m "refactor: improve Page Object structure"
git commit -m "docs: update README with test execution guide"
git commit -m "chore: update Playwright to v1.40"
.gitignore for Test Projects
.gitignore:
# Node modules
node_modules/
package-lock.json
# Test results
test-results/
playwright-report/
screenshots/
videos/
logs/
allure-results/
allure-report/
# Environment variables
.env
.env.local
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Test data
test-data/local/
⚡ GitHub Actions: Automation from Scratch
Why GitHub Actions?
Advantages:
- ✅ Free for public repositories
- ✅ 2000 minutes/month for private (free tier)
- ✅ Integration with GitHub out of the box
- ✅ Huge marketplace of workflows
- ✅ Simple YAML syntax
Basic Structure
.github/workflows/tests.yml:
name: Automated Tests
# When to run
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
schedule:
# Run every day at 9:00 UTC
- cron: '0 9 * * *'
workflow_dispatch: # Manual trigger
# What to run
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
Playwright Tests in GitHub Actions
.github/workflows/playwright.yml:
name: Playwright Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npx playwright test
- name: Upload test results
uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
- name: Upload screenshots
uses: actions/upload-artifact@v3
if: failure()
with:
name: screenshots
path: screenshots/
Parallel Testing Matrix
Running tests on different browsers in parallel:
name: Cross-Browser Testing
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
browser: [chromium, firefox, webkit]
node-version: [16, 18, 20]
steps:
- uses: actions/checkout@v3
- name: Setup Node ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: npm ci
- name: Install Playwright
run: npx playwright install --with-deps ${{ matrix.browser }}
- name: Run tests on ${{ matrix.browser }}
run: npx playwright test --project=${{ matrix.browser }}
Sharding for Fast Execution
Splitting tests into parts:
name: Sharded Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Install Playwright
run: npx playwright install --with-deps
- name: Run shard ${{ matrix.shard }} of 4
run: npx playwright test --shard=${{ matrix.shard }}/4
Environment Secrets
Working with secrets:
name: Tests with Secrets
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run tests
env:
API_KEY: ${{ secrets.API_KEY }}
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
BASE_URL: ${{ secrets.BASE_URL }}
run: npm test
Adding secrets in GitHub:
- Repo → Settings → Secrets and variables → Actions
- New repository secret
- Name:
API_KEY, Value:your-secret-key
Deploy After Successful Tests
Complete pipeline:
name: Test and Deploy
on:
push:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install and test
run: |
npm ci
npm test
# Only if tests passed
deploy:
needs: test
runs-on: ubuntu-latest
if: success()
steps:
- uses: actions/checkout@v3
- name: Deploy to staging
run: |
echo "Deploying to staging..."
# Your deploy script
- name: Run smoke tests on staging
run: npm run test:smoke
Notifications
Slack notification:
name: Tests with Notifications
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: npm ci && npm test
- name: Slack Notification on Success
if: success()
uses: rtCamp/action-slack-notify@v2
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
SLACK_CHANNEL: qa-notifications
SLACK_COLOR: good
SLACK_MESSAGE: '✅ All tests passed!'
SLACK_TITLE: Test Results
- name: Slack Notification on Failure
if: failure()
uses: rtCamp/action-slack-notify@v2
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
SLACK_CHANNEL: qa-notifications
SLACK_COLOR: danger
SLACK_MESSAGE: '❌ Tests failed! Check logs.'
SLACK_TITLE: Test Results
🔧 Jenkins: Enterprise-level CI/CD
Why Jenkins?
When to use Jenkins:
- ✅ Enterprise environments
- ✅ On-premise infrastructure
- ✅ Complex pipelines with many stages
- ✅ Integration with legacy systems
- ✅ Large teams
When to use GitHub Actions:
- ✅ Small to medium teams
- ✅ Cloud-native approach
- ✅ GitHub-centric workflow
- ✅ Simple pipelines
Jenkins Installation
Docker method (easiest):
# Download and run Jenkins
docker run -d \
--name jenkins \
-p 8080:8080 \
-p 50000:50000 \
-v jenkins_home:/var/jenkins_home \
jenkins/jenkins:lts
# Get initial admin password
docker exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword
# Open http://localhost:8080
After installation:
- Enter admin password
- Install suggested plugins
- Create admin user
- Start using Jenkins!
Jenkinsfile for Playwright
Declarative Pipeline:
pipeline {
agent any
environment {
BASE_URL = 'https://staging.example.com'
}
stages {
stage('Checkout') {
steps {
git branch: 'main',
url: 'https://github.com/username/test-automation.git'
}
}
stage('Install Dependencies') {
steps {
sh 'npm ci'
sh 'npx playwright install --with-deps'
}
}
stage('Run Tests') {
steps {
sh 'npx playwright test'
}
}
stage('Publish Results') {
steps {
publishHTML([
reportName: 'Playwright Report',
reportDir: 'playwright-report',
reportFiles: 'index.html',
keepAll: true,
alwaysLinkToLastBuild: true
])
}
}
}
post {
always {
junit 'test-results/*.xml'
archiveArtifacts artifacts: 'screenshots/**/*.png',
allowEmptyArchive: true
}
failure {
emailext(
subject: "❌ Tests Failed: ${env.JOB_NAME} - ${env.BUILD_NUMBER}",
body: """
Build failed!
Job: ${env.JOB_NAME}
Build: ${env.BUILD_NUMBER}
URL: ${env.BUILD_URL}
""",
to: 'qa-team@example.com'
)
}
success {
echo '✅ All tests passed!'
}
}
}
Multi-stage Pipeline
Complex pipeline with different test types:
pipeline {
agent any
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Build') {
steps {
sh 'npm ci'
}
}
stage('Unit Tests') {
steps {
sh 'npm run test:unit'
}
}
stage('Integration Tests') {
parallel {
stage('API Tests') {
steps {
sh 'npm run test:api'
}
}
stage('Component Tests') {
steps {
sh 'npm run test:component'
}
}
}
}
stage('E2E Tests') {
steps {
sh 'npx playwright test'
}
}
stage('Deploy to Staging') {
when {
branch 'main'
expression { currentBuild.result == null || currentBuild.result == 'SUCCESS' }
}
steps {
sh './deploy-staging.sh'
}
}
stage('Smoke Tests on Staging') {
when {
branch 'main'
}
steps {
sh 'npm run test:smoke'
}
}
}
post {
always {
publishHTML([
reportName: 'Test Report',
reportDir: 'playwright-report',
reportFiles: 'index.html'
])
junit 'test-results/**/*.xml'
}
failure {
slackSend(
channel: '#qa-notifications',
color: 'danger',
message: "❌ Build Failed: ${env.JOB_NAME} ${env.BUILD_NUMBER}"
)
}
success {
slackSend(
channel: '#qa-notifications',
color: 'good',
message: "✅ Build Successful: ${env.JOB_NAME} ${env.BUILD_NUMBER}"
)
}
}
}
Jenkins Plugins for QA
Essential Plugins:
# Install via Jenkins UI:
# Manage Jenkins → Manage Plugins → Available
1. HTML Publisher Plugin # Test reports
2. JUnit Plugin # Test results
3. Slack Notification Plugin # Notifications
4. Blue Ocean # Modern UI
5. Pipeline # Pipeline support
6. Git Plugin # Git integration
7. Docker Plugin # Docker integration
8. Allure Plugin # Beautiful reports
Scheduled Tests (Cron)
Running tests on schedule:
pipeline {
agent any
triggers {
// Every day at 9:00
cron('0 9 * * *')
// Every hour
// cron('0 * * * *')
// Every Monday at 8:00
// cron('0 8 * * 1')
}
stages {
stage('Nightly Tests') {
steps {
sh 'npx playwright test --project=full-suite'
}
}
}
}
🐳 Docker for Test Automation
Why Docker for Tests?
Problems without Docker:
- ❌ “Works on my machine”
- ❌ Environment dependency
- ❌ Complex CI setup
- ❌ Inconsistent browser versions
With Docker:
- ✅ Consistent environment
- ✅ Easy setup
- ✅ Isolated execution
- ✅ Environment versioning
Dockerfile for Playwright
Dockerfile:
FROM mcr.microsoft.com/playwright:v1.40.0-focal
# Set working directory
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci
# Copy test files
COPY . .
# Run tests by default
CMD ["npx", "playwright", "test"]
docker-compose.yml:
version: '3.8'
services:
tests:
build: .
environment:
- BASE_URL=https://staging.example.com
- CI=true
volumes:
- ./test-results:/app/test-results
- ./playwright-report:/app/playwright-report
- ./screenshots:/app/screenshots
Running tests:
# Build image
docker build -t my-tests .
# Run tests
docker run --rm my-tests
# Run with docker-compose
docker-compose up --abort-on-container-exit
# Run specific test
docker run --rm my-tests npx playwright test tests/login.spec.js
Multi-stage Dockerfile (optimization)
# Stage 1: Dependencies
FROM node:18-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
# Stage 2: Test dependencies
FROM mcr.microsoft.com/playwright:v1.40.0-focal AS test-deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
# Stage 3: Runtime
FROM mcr.microsoft.com/playwright:v1.40.0-focal
WORKDIR /app
# Copy dependencies
COPY --from=test-deps /app/node_modules ./node_modules
COPY . .
# Run tests
CMD ["npx", "playwright", "test"]
Docker in Jenkins
Jenkinsfile with Docker:
pipeline {
agent {
docker {
image 'mcr.microsoft.com/playwright:v1.40.0-focal'
args '-v $HOME/.npm:/root/.npm'
}
}
stages {
stage('Test') {
steps {
sh 'npm ci'
sh 'npx playwright test'
}
}
}
}
Docker Compose for Integration Tests
docker-compose.test.yml:
version: '3.8'
services:
# Your application
app:
build: ../app
ports:
- "3000:3000"
environment:
- NODE_ENV=test
- DB_HOST=db
depends_on:
- db
# Database
db:
image: postgres:15
environment:
POSTGRES_DB: testdb
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpass
ports:
- "5432:5432"
# Tests
tests:
build: .
environment:
- BASE_URL=http://app:3000
- DB_URL=postgresql://testuser:testpass@db:5432/testdb
depends_on:
- app
- db
command: sh -c "sleep 5 && npx playwright test"
Running:
docker-compose -f docker-compose.test.yml up --abort-on-container-exit
🔗 Integrating Tests into Pipeline
Test Pyramid in CI/CD
Pipeline Stages:
┌─────────────────────────────────────────┐
│ 1. Unit Tests (30 sec) │
│ ✓ Fast │
│ ✓ No dependencies │
│ ✓ Run on every commit │
├─────────────────────────────────────────┤
│ 2. Integration Tests (2-5 min) │
│ ✓ API tests │
│ ✓ Component tests │
│ ✓ Database tests │
├─────────────────────────────────────────┤
│ 3. E2E Tests (10-15 min) │
│ ✓ Critical user flows │
│ ✓ Smoke tests │
│ ✓ Visual regression │
├─────────────────────────────────────────┤
│ 4. Full Regression (30-60 min) │
│ ✓ Nightly builds only │
│ ✓ All test suites │
│ ✓ Multiple environments │
└─────────────────────────────────────────┘
Test Parallelization
package.json scripts:
{
"scripts": {
"test": "playwright test",
"test:unit": "jest",
"test:api": "newman run postman-collection.json",
"test:e2e": "playwright test tests/e2e",
"test:smoke": "playwright test tests/smoke --grep @smoke",
"test:parallel": "playwright test --workers=4",
"test:headed": "playwright test --headed",
"test:debug": "playwright test --debug"
}
}
GitHub Actions with parallelization:
name: Parallel Tests
on: [push]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
suite: [smoke, auth, checkout, products, admin]
steps:
- uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Run ${{ matrix.suite }} tests
run: npx playwright test tests/${{ matrix.suite }}
Conditional Testing
Running tests depending on changes:
name: Smart Testing
on: [push]
jobs:
detect-changes:
runs-on: ubuntu-latest
outputs:
frontend: ${{ steps.filter.outputs.frontend }}
backend: ${{ steps.filter.outputs.backend }}
steps:
- uses: actions/checkout@v3
- uses: dorny/paths-filter@v2
id: filter
with:
filters: |
frontend:
- 'src/frontend/**'
- 'tests/e2e/**'
backend:
- 'src/backend/**'
- 'tests/api/**'
test-frontend:
needs: detect-changes
if: needs.detect-changes.outputs.frontend == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: npm ci && npm run test:e2e
test-backend:
needs: detect-changes
if: needs.detect-changes.outputs.backend == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: npm ci && npm run test:api
📊 Test Reporting & Dashboards
Playwright HTML Reporter
playwright.config.js:
export default {
reporter: [
['html', {
outputFolder: 'playwright-report',
open: 'never'
}],
['json', {
outputFile: 'test-results.json'
}],
['junit', {
outputFile: 'junit-results.xml'
}],
['list'] // Console output
],
};
Viewing reports in CI:
# GitHub Actions
- name: Upload HTML report
uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
Allure Reports
The most beautiful reports!
Installation:
npm install --save-dev @playwright/test allure-playwright
playwright.config.js:
export default {
reporter: [
['allure-playwright', {
detail: true,
outputFolder: 'allure-results',
suiteTitle: false
}]
],
};
Generating report:
# Install Allure CLI
npm install -g allure-commandline
# Run tests
npx playwright test
# Generate report
allure generate allure-results -o allure-report --clean
# Open report
allure open allure-report
In CI:
# GitHub Actions
- name: Run tests
run: npx playwright test
- name: Generate Allure Report
if: always()
run: |
npm install -g allure-commandline
allure generate allure-results -o allure-report --clean
- name: Deploy report to GitHub Pages
if: always()
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./allure-report
Custom Test Dashboard
dashboard.js - Simple Dashboard:
const fs = require('fs');
const path = require('path');
class TestDashboard {
constructor() {
this.results = [];
}
parseTestResults(resultsPath) {
const data = JSON.parse(fs.readFileSync(resultsPath, 'utf8'));
const stats = {
total: 0,
passed: 0,
failed: 0,
skipped: 0,
duration: 0,
tests: []
};
data.suites.forEach(suite => {
suite.specs.forEach(spec => {
spec.tests.forEach(test => {
stats.total++;
stats.duration += test.results[0].duration;
if (test.results[0].status === 'passed') {
stats.passed++;
} else if (test.results[0].status === 'failed') {
stats.failed++;
} else {
stats.skipped++;
}
stats.tests.push({
name: spec.title,
suite: suite.title,
status: test.results[0].status,
duration: test.results[0].duration,
error: test.results[0].error?.message
});
});
});
});
return stats;
}
generateHTML(stats) {
const passRate = ((stats.passed / stats.total) * 100).toFixed(1);
return `
<!DOCTYPE html>
<html>
<head>
<title>Test Dashboard</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
background: #f5f5f5;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
padding: 20px;
border-radius: 8px;
text-align: center;
}
.stat-card.total { background: #e3f2fd; }
.stat-card.passed { background: #c8e6c9; }
.stat-card.failed { background: #ffcdd2; }
.stat-card.skipped { background: #fff9c4; }
.stat-value {
font-size: 48px;
font-weight: bold;
margin: 10px 0;
}
.stat-label {
font-size: 14px;
color: #666;
text-transform: uppercase;
}
.progress-bar {
width: 100%;
height: 30px;
background: #e0e0e0;
border-radius: 15px;
overflow: hidden;
margin: 20px 0;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #4caf50, #8bc34a);
text-align: center;
line-height: 30px;
color: white;
font-weight: bold;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #ddd;
}
th {
background: #f5f5f5;
font-weight: bold;
}
.status {
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: bold;
}
.status.passed { background: #c8e6c9; color: #2e7d32; }
.status.failed { background: #ffcdd2; color: #c62828; }
.status.skipped { background: #fff9c4; color: #f57f17; }
</style>
</head>
<body>
<div class="container">
<h1>Test Execution Dashboard</h1>
<p>Generated: ${new Date().toLocaleString()}</p>
<div class="stats">
<div class="stat-card total">
<div class="stat-label">Total Tests</div>
<div class="stat-value">${stats.total}</div>
</div>
<div class="stat-card passed">
<div class="stat-label">Passed</div>
<div class="stat-value">${stats.passed}</div>
</div>
<div class="stat-card failed">
<div class="stat-label">Failed</div>
<div class="stat-value">${stats.failed}</div>
</div>
<div class="stat-card skipped">
<div class="stat-label">Skipped</div>
<div class="stat-value">${stats.skipped}</div>
</div>
</div>
<div class="progress-bar">
<div class="progress-fill" style="width: ${passRate}%">
${passRate}% Pass Rate
</div>
</div>
<h2>Test Results</h2>
<table>
<thead>
<tr>
<th>Suite</th>
<th>Test</th>
<th>Status</th>
<th>Duration</th>
<th>Error</th>
</tr>
</thead>
<tbody>
${stats.tests.map(test => `
<tr>
<td>${test.suite}</td>
<td>${test.name}</td>
<td><span class="status ${test.status}">${test.status}</span></td>
<td>${(test.duration / 1000).toFixed(2)}s</td>
<td>${test.error || '-'}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
</body>
</html>
`;
}
generate(resultsPath, outputPath) {
const stats = this.parseTestResults(resultsPath);
const html = this.generateHTML(stats);
fs.writeFileSync(outputPath, html);
console.log(\`Dashboard generated: \${outputPath}\`);
return stats;
}
}
// Usage
const dashboard = new TestDashboard();
dashboard.generate('test-results.json', 'dashboard.html');
In package.json:
{
"scripts": {
"test:report": "npx playwright test && node dashboard.js"
}
}
🔔 Notifications & Monitoring
Slack Integration
Setting up Slack App:
- Create Slack App: https://api.slack.com/apps
- Enable “Incoming Webhooks”
- Copy Webhook URL
slack-notifier.js:
const axios = require('axios');
class SlackNotifier {
constructor(webhookUrl) {
this.webhookUrl = webhookUrl;
}
async sendTestResults(stats, buildUrl) {
const passRate = ((stats.passed / stats.total) * 100).toFixed(1);
const color = stats.failed === 0 ? 'good' : 'danger';
const message = {
text: stats.failed === 0
? '✅ All tests passed!'
: `❌ ${stats.failed} test(s) failed`,
attachments: [{
color: color,
title: 'Test Execution Report',
fields: [
{
title: 'Total Tests',
value: stats.total.toString(),
short: true
},
{
title: 'Passed',
value: `✅ ${stats.passed}`,
short: true
},
{
title: 'Failed',
value: `❌ ${stats.failed}`,
short: true
},
{
title: 'Pass Rate',
value: `${passRate}%`,
short: true
},
{
title: 'Duration',
value: `${(stats.duration / 1000 / 60).toFixed(1)} min`,
short: true
}
],
footer: 'Test Automation',
footer_icon: 'https://playwright.dev/img/playwright-logo.svg',
ts: Math.floor(Date.now() / 1000)
}]
};
if (buildUrl) {
message.attachments[0].actions = [{
type: 'button',
text: 'View Details',
url: buildUrl
}];
}
try {
await axios.post(this.webhookUrl, message);
console.log('✅ Slack notification sent');
} catch (error) {
console.error('❌ Failed to send Slack notification:', error.message);
}
}
async sendFailedTests(failedTests) {
if (failedTests.length === 0) return;
const message = {
text: '❌ Failed Tests Details',
attachments: failedTests.map(test => ({
color: 'danger',
title: test.name,
text: test.error || 'No error message',
fields: [
{
title: 'Suite',
value: test.suite,
short: true
},
{
title: 'Duration',
value: `${(test.duration / 1000).toFixed(2)}s`,
short: true
}
]
}))
};
try {
await axios.post(this.webhookUrl, message);
} catch (error) {
console.error('Failed to send details:', error.message);
}
}
}
module.exports = SlackNotifier;
In CI:
# GitHub Actions
- name: Send Slack Notification
if: always()
run: |
node scripts/slack-notifier.js
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
Email Notifications
email-notifier.js:
const nodemailer = require('nodemailer');
class EmailNotifier {
constructor(config) {
this.transporter = nodemailer.createTransport({
host: config.smtp.host,
port: config.smtp.port,
secure: true,
auth: {
user: config.smtp.user,
pass: config.smtp.password
}
});
}
async sendTestResults(stats, recipients) {
const passRate = ((stats.passed / stats.total) * 100).toFixed(1);
const subject = stats.failed === 0
? `✅ All Tests Passed (${passRate}%)`
: `❌ ${stats.failed} Tests Failed`;
const html = `
<h2>Test Execution Report</h2>
<table style="border-collapse: collapse;">
<tr>
<td style="padding: 10px; border: 1px solid #ddd;"><strong>Total Tests:</strong></td>
<td style="padding: 10px; border: 1px solid #ddd;">${stats.total}</td>
</tr>
<tr>
<td style="padding: 10px; border: 1px solid #ddd;"><strong>Passed:</strong></td>
<td style="padding: 10px; border: 1px solid #ddd; color: green;">✅ ${stats.passed}</td>
</tr>
<tr>
<td style="padding: 10px; border: 1px solid #ddd;"><strong>Failed:</strong></td>
<td style="padding: 10px; border: 1px solid #ddd; color: red;">❌ ${stats.failed}</td>
</tr>
<tr>
<td style="padding: 10px; border: 1px solid #ddd;"><strong>Pass Rate:</strong></td>
<td style="padding: 10px; border: 1px solid #ddd;">${passRate}%</td>
</tr>
</table>
${stats.failed > 0 ? `
<h3>Failed Tests:</h3>
<ul>
${stats.tests
.filter(t => t.status === 'failed')
.map(t => `<li>${t.suite} - ${t.name}</li>`)
.join('')}
</ul>
` : ''}
`;
await this.transporter.sendMail({
from: '"Test Automation" <noreply@example.com>',
to: recipients.join(', '),
subject: subject,
html: html
});
}
}
module.exports = EmailNotifier;
Microsoft Teams Integration
teams-notifier.js:
const axios = require('axios');
class TeamsNotifier {
constructor(webhookUrl) {
this.webhookUrl = webhookUrl;
}
async sendTestResults(stats, buildUrl) {
const passRate = ((stats.passed / stats.total) * 100).toFixed(1);
const themeColor = stats.failed === 0 ? '00FF00' : 'FF0000';
const card = {
"@type": "MessageCard",
"@context": "https://schema.org/extensions",
"summary": "Test Results",
"themeColor": themeColor,
"title": stats.failed === 0
? "✅ All Tests Passed"
: `❌ ${stats.failed} Tests Failed`,
"sections": [{
"activityTitle": "Test Execution Report",
"facts": [
{ "name": "Total Tests:", "value": stats.total },
{ "name": "Passed:", "value": `✅ ${stats.passed}` },
{ "name": "Failed:", "value": `❌ ${stats.failed}` },
{ "name": "Pass Rate:", "value": `${passRate}%` },
{ "name": "Duration:", "value": `${(stats.duration / 60000).toFixed(1)} min` }
]
}]
};
if (buildUrl) {
card.potentialAction = [{
"@type": "OpenUri",
"name": "View Report",
"targets": [{ "os": "default", "uri": buildUrl }]
}];
}
await axios.post(this.webhookUrl, card);
}
}
module.exports = TeamsNotifier;
🎯 Real Project: Complete CI/CD Setup
Project Structure
ecommerce-ci-cd/
├── .github/
│ └── workflows/
│ ├── pr-tests.yml # PR validation
│ ├── nightly-tests.yml # Full regression
│ └── deploy.yml # Deploy pipeline
├── tests/
│ ├── e2e/
│ ├── api/
│ └── smoke/
├── scripts/
│ ├── slack-notifier.js
│ ├── dashboard.js
│ └── cleanup.js
├── Dockerfile
├── docker-compose.yml
├── Jenkinsfile
├── playwright.config.js
└── package.json
Complete GitHub Actions Workflow
.github/workflows/complete-pipeline.yml:
name: Complete CI/CD Pipeline
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
schedule:
- cron: '0 0 * * *' # Nightly at midnight
workflow_dispatch:
env:
NODE_VERSION: '18'
jobs:
# Job 1: Lint & Static Analysis
lint:
name: Code Quality
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install dependencies
run: npm ci
- name: Run ESLint
run: npm run lint
- name: Check code formatting
run: npm run format:check
# Job 2: Unit Tests
unit-tests:
name: Unit Tests
runs-on: ubuntu-latest
needs: lint
steps:
- uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install dependencies
run: npm ci
- name: Run unit tests
run: npm run test:unit
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
# Job 3: API Tests
api-tests:
name: API Tests
runs-on: ubuntu-latest
needs: lint
steps:
- uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install dependencies
run: npm ci
- name: Run API tests
run: npm run test:api
env:
API_URL: ${{ secrets.API_URL }}
API_KEY: ${{ secrets.API_KEY }}
# Job 4: E2E Tests (Sharded)
e2e-tests:
name: E2E Tests (Shard ${{ matrix.shard }})
runs-on: ubuntu-latest
needs: [unit-tests, api-tests]
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install dependencies
run: npm ci
- name: Install Playwright
run: npx playwright install --with-deps
- name: Run E2E tests
run: npx playwright test --shard=${{ matrix.shard }}/4
env:
BASE_URL: ${{ secrets.STAGING_URL }}
- name: Upload test results
if: always()
uses: actions/upload-artifact@v3
with:
name: playwright-report-${{ matrix.shard }}
path: playwright-report/
retention-days: 30
- name: Upload screenshots
if: failure()
uses: actions/upload-artifact@v3
with:
name: screenshots-${{ matrix.shard }}
path: screenshots/
# Job 5: Generate Reports
report:
name: Generate Test Report
runs-on: ubuntu-latest
needs: e2e-tests
if: always()
steps:
- uses: actions/checkout@v3
- name: Download all artifacts
uses: actions/download-artifact@v3
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install dependencies
run: npm ci
- name: Generate dashboard
run: node scripts/dashboard.js
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./dashboard
# Job 6: Notify
notify:
name: Send Notifications
runs-on: ubuntu-latest
needs: [e2e-tests]
if: always()
steps:
- uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install dependencies
run: npm ci
- name: Send Slack notification
run: node scripts/slack-notifier.js
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
BUILD_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
# Job 7: Deploy (only on main branch if all tests passed)
deploy:
name: Deploy to Staging
runs-on: ubuntu-latest
needs: [e2e-tests]
if: github.ref == 'refs/heads/main' && success()
steps:
- uses: actions/checkout@v3
- name: Deploy to staging
run: |
echo "Deploying to staging..."
# Your deployment script
- name: Run smoke tests
run: npm run test:smoke
env:
BASE_URL: ${{ secrets.STAGING_URL }}
Complete Jenkinsfile
Jenkinsfile:
pipeline {
agent any
environment {
NODE_VERSION = '18'
STAGING_URL = credentials('staging-url')
API_KEY = credentials('api-key')
SLACK_WEBHOOK = credentials('slack-webhook')
}
options {
timeout(time: 1, unit: 'HOURS')
timestamps()
buildDiscarder(logRotator(numToKeepStr: '30'))
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Setup') {
steps {
sh """
node --version
npm --version
npm ci
"""
}
}
stage('Code Quality') {
parallel {
stage('Lint') {
steps {
sh 'npm run lint'
}
}
stage('Format Check') {
steps {
sh 'npm run format:check'
}
}
}
}
stage('Unit Tests') {
steps {
sh 'npm run test:unit'
}
post {
always {
junit 'test-results/unit/*.xml'
}
}
}
stage('API Tests') {
steps {
sh 'npm run test:api'
}
post {
always {
junit 'test-results/api/*.xml'
}
}
}
stage('E2E Tests') {
steps {
sh '''
npx playwright install --with-deps
npx playwright test
'''
}
post {
always {
publishHTML([
reportName: 'Playwright Report',
reportDir: 'playwright-report',
reportFiles: 'index.html',
keepAll: true
])
junit 'test-results/e2e/*.xml'
archiveArtifacts artifacts: 'screenshots/**/*.png',
allowEmptyArchive: true
}
}
}
stage('Generate Dashboard') {
when {
expression { currentBuild.result == null || currentBuild.result == 'SUCCESS' }
}
steps {
sh 'node scripts/dashboard.js'
publishHTML([
reportName: 'Test Dashboard',
reportDir: 'dashboard',
reportFiles: 'index.html',
keepAll: true
])
}
}
stage('Deploy to Staging') {
when {
branch 'main'
expression { currentBuild.result == null || currentBuild.result == 'SUCCESS' }
}
steps {
sh './deploy-staging.sh'
}
}
stage('Smoke Tests on Staging') {
when {
branch 'main'
}
steps {
sh 'npm run test:smoke'
}
}
}
post {
always {
sh 'node scripts/slack-notifier.js'
}
failure {
emailext(
subject: "❌ Build Failed: ${env.JOB_NAME} #${env.BUILD_NUMBER}",
body: """
Build failed!
Job: ${env.JOB_NAME}
Build: ${env.BUILD_NUMBER}
URL: ${env.BUILD_URL}
Check the console output for details.
""",
to: 'qa-team@example.com'
)
}
success {
echo '✅ All stages completed successfully!'
}
}
}
🎓 Learning Resources
📺 YouTube Channels
CI/CD:
-
TechWorld with Nana
- YouTube
- ⭐⭐⭐⭐⭐ Best DevOps channel
- Jenkins, Docker, Kubernetes
-
FreeCodeCamp - Jenkins Full Course
- YouTube
- 4+ hours of free content
-
Automation Step by Step
- YouTube
- Jenkins for QA
Docker:
💻 Online Courses
GitHub Actions:
-
“GitHub Actions - The Complete Guide” - Academind
- 💰 Udemy: $12.99
- 6+ hours
- 💰 Get Course - 85% OFF
-
GitHub Learning Lab (FREE!)
- 🆓 Interactive tutorials
- GitHub Skills
Jenkins:
-
“Jenkins From Zero To Hero” - Udemy
- 💰 $13.99
- Complete Jenkins guide
- 💰 Get Course
-
“Learn DevOps: CI/CD with Jenkins Pipelines” - Udemy
- 💰 $12.99
- Hands-on projects
- 💰 Get Course
Docker:
-
“Docker Mastery” - Bret Fisher
- 💰 Udemy: $12.99
- 19+ hours
- Industry standard
- 💰 Get Course
-
“Docker for QA Engineers” - Test Automation University
- 🆓 FREE
- TAU - Docker Course
LinkedIn Learning:
-
“Learning GitHub Actions”
- 1 month free
- 💰 LinkedIn Learning
-
“Continuous Integration and Continuous Delivery (CI/CD)”
- Overview course
- LinkedIn Learning
📚 Books
1. “Continuous Delivery” - Jez Humble, David Farley
- CI/CD bible
- Must-read for everyone
- 💰 Amazon - $45
2. “The Phoenix Project” - Gene Kim
- DevOps novel
- Understanding culture
- 💰 Amazon - $18
3. “Docker Deep Dive” - Nigel Poulton
- Comprehensive Docker guide
- 💰 Amazon - $25
🏆 Practical Resources
1. Play with Docker
- 🆓 Free Docker playground
- labs.play-with-docker.com
2. Katacoda/KillerCoda
- 🆓 Interactive scenarios
- Jenkins, Docker, Kubernetes
- killercoda.com
3. GitHub Actions Marketplace
- 10,000+ ready actions
- github.com/marketplace
🎓 Certifications
1. GitHub Actions Certification
- $99
- Official certification
- GitHub Certifications
2. Jenkins Engineer Certification
- $150
- CloudBees University
3. Docker Certified Associate
- $195
- Docker Certification
✅ Checklist: CI/CD Readiness
Git/GitHub:
- Understand Git workflow
- Can work with branches
- Can create Pull Request
- Know how to do code review
- Understand Git conflicts resolution
GitHub Actions:
- Wrote basic workflow
- Configured matrix strategy
- Worked with secrets
- Know how to use artifacts
- Configured notifications
Jenkins:
- Installed Jenkins locally
- Wrote Jenkinsfile
- Configured Jenkins pipeline
- Familiar with Jenkins plugins
- Configured scheduled builds
Docker:
- Wrote Dockerfile
- Created docker-compose
- Ran tests in container
- Understand Docker networking
- Worked with Docker volumes
Reporting:
- Configured HTML reports
- Integrated Allure
- Created custom dashboard
- Configured notifications
General:
- Understand CI/CD principles
- Can explain test pyramid
- Know best practices
- Have project with CI/CD in GitHub
📍 What’s Next?
In the next article we’ll cover Performance Testing:
- JMeter for load testing
- K6 for modern performance testing
- Gatling advanced scenarios
- Performance monitoring
- Analyzing results
Next article: Article 6: Performance Testing
💡 Final Advice for Apple Interview
What Apple values in CI/CD:
-
Automation First
- Everything should be automated
- Minimal manual steps
-
Fast Feedback
- Tests are fast (< 10 min)
- Parallel execution
- Smart test selection
-
Reliability
- Stable tests (no flaky tests)
- Retry mechanisms
- Clear failure reporting
-
Observability
- Good dashboards
- Metrics tracking
- Trend analysis
Show in interview:
- ✅ GitHub repo with CI/CD setup
- ✅ Jenkinsfile examples
- ✅ Docker containerized tests
- ✅ Test reports/dashboards
- ✅ Notifications setup
Be ready to explain:
- Why you chose specific approach
- How you handle flaky tests
- How you optimize execution time
- How you monitor quality
Was this article helpful? 👏
Questions? Write in comments!
Author: AAnnayev — Senior SDET Tags: #CICD #Jenkins #GitHubActions #Docker #DevOps #Apple #QA
P.S. Full project code with CI/CD: github.com/yourname/cicd-test-framework
P.P.S. Only 2 articles to the finale! Article 6 (Performance) and Article 7 (Apple Job)! 🚀